﻿
/*!
 long.js (c) 2013 Daniel Wirtz <dcode@dcode.io>
 Released under the Apache License, Version 2.0
 see: https://github.com/dcodeIO/long.js for details
*/
(function () {
    /**
     * Constructs a 64 bit two's-complement integer, given its low and high 32 bit values as *signed* integers.
     *  See the from* functions below for more convenient ways of constructing Longs.
     * @exports Long
     * @class A Long class for representing a 64 bit two's-complement integer value.
     * @param {number} low The low (signed) 32 bits of the long
     * @param {number} high The high (signed) 32 bits of the long
     * @param {boolean=} unsigned Whether unsigned or not, defaults to `false` for signed
     * @constructor
     */
    function Long(low, high, unsigned) {

        /**
         * The low 32 bits as a signed value.
         * @type {number}
         * @expose
         */
        this.low = low | 0;

        /**
         * The high 32 bits as a signed value.
         * @type {number}
         * @expose
         */
        this.high = high | 0;

        /**
         * Whether unsigned or not.
         * @type {boolean}
         * @expose
         */
        this.unsigned = !!unsigned;
    }

    // The internal representation of a long is the two given signed, 32-bit values.
    // We use 32-bit pieces because these are the size of integers on which
    // Javascript performs bit-operations.  For operations like addition and
    // multiplication, we split each number into 16 bit pieces, which can easily be
    // multiplied within Javascript's floating-point representation without overflow
    // or change in sign.
    //
    // In the algorithms below, we frequently reduce the negative case to the
    // positive case by negating the input(s) and then post-processing the result.
    // Note that we must ALWAYS check specially whether those values are MIN_VALUE
    // (-2^63) because -MIN_VALUE == MIN_VALUE (since 2^63 cannot be represented as
    // a positive number, it overflows back into a negative).  Not handling this
    // case would often result in infinite recursion.
    //
    // Common constant values ZERO, ONE, NEG_ONE, etc. are defined below the from*
    // methods on which they depend.

    /**
     * An indicator used to reliably determine if an object is a Long or not.
     * @type {boolean}
     * @const
     * @expose
     * @private
     */
    Long.prototype.__isLong__;

    Object.defineProperty(Long.prototype, "__isLong__", {
        value: true,
        enumerable: false,
        configurable: false
    });

    /**
     * @function
     * @param {*} obj Object
     * @returns {boolean}
     * @inner
     */
    function isLong(obj) {
        return (obj && obj["__isLong__"]) === true;
    }

    /**
     * Tests if the specified object is a Long.
     * @function
     * @param {*} obj Object
     * @returns {boolean}
     * @expose
     */
    Long.isLong = isLong;

    /**
     * A cache of the Long representations of small integer values.
     * @type {!Object}
     * @inner
     */
    var INT_CACHE = {};

    /**
     * A cache of the Long representations of small unsigned integer values.
     * @type {!Object}
     * @inner
     */
    var UINT_CACHE = {};

    /**
     * @param {number} value
     * @param {boolean=} unsigned
     * @returns {!Long}
     * @inner
     */
    function fromInt(value, unsigned) {
        var obj, cachedObj, cache;
        if (unsigned) {
            value >>>= 0;
            if (cache = (0 <= value && value < 256)) {
                cachedObj = UINT_CACHE[value];
                if (cachedObj)
                    return cachedObj;
            }
            obj = fromBits(value, (value | 0) < 0 ? -1 : 0, true);
            if (cache)
                UINT_CACHE[value] = obj;
            return obj;
        } else {
            value |= 0;
            if (cache = (-128 <= value && value < 128)) {
                cachedObj = INT_CACHE[value];
                if (cachedObj)
                    return cachedObj;
            }
            obj = fromBits(value, value < 0 ? -1 : 0, false);
            if (cache)
                INT_CACHE[value] = obj;
            return obj;
        }
    }

    /**
     * Returns a Long representing the given 32 bit integer value.
     * @function
     * @param {number} value The 32 bit integer in question
     * @param {boolean=} unsigned Whether unsigned or not, defaults to `false` for signed
     * @returns {!Long} The corresponding Long value
     * @expose
     */
    Long.fromInt = fromInt;

    /**
     * @param {number} value
     * @param {boolean=} unsigned
     * @returns {!Long}
     * @inner
     */
    function fromNumber(value, unsigned) {
        if (isNaN(value) || !isFinite(value))
            return unsigned ? UZERO : ZERO;
        if (unsigned) {
            if (value < 0)
                return UZERO;
            if (value >= TWO_PWR_64_DBL)
                return MAX_UNSIGNED_VALUE;
        } else {
            if (value <= -TWO_PWR_63_DBL)
                return MIN_VALUE;
            if (value + 1 >= TWO_PWR_63_DBL)
                return MAX_VALUE;
        }
        if (value < 0)
            return fromNumber(-value, unsigned).neg();
        return fromBits((value % TWO_PWR_32_DBL) | 0, (value / TWO_PWR_32_DBL) | 0, unsigned);
    }

    /**
     * Returns a Long representing the given value, provided that it is a finite number. Otherwise, zero is returned.
     * @function
     * @param {number} value The number in question
     * @param {boolean=} unsigned Whether unsigned or not, defaults to `false` for signed
     * @returns {!Long} The corresponding Long value
     * @expose
     */
    Long.fromNumber = fromNumber;

    /**
     * @param {number} lowBits
     * @param {number} highBits
     * @param {boolean=} unsigned
     * @returns {!Long}
     * @inner
     */
    function fromBits(lowBits, highBits, unsigned) {
        return new Long(lowBits, highBits, unsigned);
    }

    /**
     * Returns a Long representing the 64 bit integer that comes by concatenating the given low and high bits. Each is
     *  assumed to use 32 bits.
     * @function
     * @param {number} lowBits The low 32 bits
     * @param {number} highBits The high 32 bits
     * @param {boolean=} unsigned Whether unsigned or not, defaults to `false` for signed
     * @returns {!Long} The corresponding Long value
     * @expose
     */
    Long.fromBits = fromBits;

    /**
     * @function
     * @param {number} base
     * @param {number} exponent
     * @returns {number}
     * @inner
     */
    var pow_dbl = Math.pow; // Used 4 times (4*8 to 15+4)

    /**
     * @param {string} str
     * @param {(boolean|number)=} unsigned
     * @param {number=} radix
     * @returns {!Long}
     * @inner
     */
    function fromString(str, unsigned, radix) {
        if (str.length === 0)
            throw Error('empty string');
        if (str === "NaN" || str === "Infinity" || str === "+Infinity" || str === "-Infinity")
            return ZERO;
        if (typeof unsigned === 'number') {
            // For goog.math.long compatibility
            radix = unsigned,
                unsigned = false;
        } else {
            unsigned = !!unsigned;
        }
        radix = radix || 10;
        if (radix < 2 || 36 < radix)
            throw RangeError('radix');

        var p;
        if ((p = str.indexOf('-')) > 0)
            throw Error('interior hyphen');
        else if (p === 0) {
            return fromString(str.substring(1), unsigned, radix).neg();
        }

        // Do several (8) digits each time through the loop, so as to
        // minimize the calls to the very expensive emulated div.
        var radixToPower = fromNumber(pow_dbl(radix, 8));

        var result = ZERO;
        for (var i = 0; i < str.length; i += 8) {
            var size = Math.min(8, str.length - i),
                value = parseInt(str.substring(i, i + size), radix);
            if (size < 8) {
                var power = fromNumber(pow_dbl(radix, size));
                result = result.mul(power).add(fromNumber(value));
            } else {
                result = result.mul(radixToPower);
                result = result.add(fromNumber(value));
            }
        }
        result.unsigned = unsigned;
        return result;
    }

    /**
     * Returns a Long representation of the given string, written using the specified radix.
     * @function
     * @param {string} str The textual representation of the Long
     * @param {(boolean|number)=} unsigned Whether unsigned or not, defaults to `false` for signed
     * @param {number=} radix The radix in which the text is written (2-36), defaults to 10
     * @returns {!Long} The corresponding Long value
     * @expose
     */
    Long.fromString = fromString;

    /**
     * @function
     * @param {!Long|number|string|!{low: number, high: number, unsigned: boolean}} val
     * @returns {!Long}
     * @inner
     */
    function fromValue(val) {
        if (val /* is compatible */ instanceof Long)
            return val;
        if (typeof val === 'number')
            return fromNumber(val);
        if (typeof val === 'string')
            return fromString(val);
        // Throws for non-objects, converts non-instanceof Long:
        return fromBits(val.low, val.high, val.unsigned);
    }

    /**
     * Converts the specified value to a Long.
     * @function
     * @param {!Long|number|string|!{low: number, high: number, unsigned: boolean}} val Value
     * @returns {!Long}
     * @expose
     */
    Long.fromValue = fromValue;

    // NOTE: the compiler should inline these constant values below and then remove these variables, so there should be
    // no runtime penalty for these.

    /**
     * @type {number}
     * @const
     * @inner
     */
    var TWO_PWR_16_DBL = 1 << 16;

    /**
     * @type {number}
     * @const
     * @inner
     */
    var TWO_PWR_24_DBL = 1 << 24;

    /**
     * @type {number}
     * @const
     * @inner
     */
    var TWO_PWR_32_DBL = TWO_PWR_16_DBL * TWO_PWR_16_DBL;

    /**
     * @type {number}
     * @const
     * @inner
     */
    var TWO_PWR_64_DBL = TWO_PWR_32_DBL * TWO_PWR_32_DBL;

    /**
     * @type {number}
     * @const
     * @inner
     */
    var TWO_PWR_63_DBL = TWO_PWR_64_DBL / 2;

    /**
     * @type {!Long}
     * @const
     * @inner
     */
    var TWO_PWR_24 = fromInt(TWO_PWR_24_DBL);

    /**
     * @type {!Long}
     * @inner
     */
    var ZERO = fromInt(0);

    /**
     * Signed zero.
     * @type {!Long}
     * @expose
     */
    Long.ZERO = ZERO;

    /**
     * @type {!Long}
     * @inner
     */
    var UZERO = fromInt(0, true);

    /**
     * Unsigned zero.
     * @type {!Long}
     * @expose
     */
    Long.UZERO = UZERO;

    /**
     * @type {!Long}
     * @inner
     */
    var ONE = fromInt(1);

    /**
     * Signed one.
     * @type {!Long}
     * @expose
     */
    Long.ONE = ONE;

    /**
     * @type {!Long}
     * @inner
     */
    var UONE = fromInt(1, true);

    /**
     * Unsigned one.
     * @type {!Long}
     * @expose
     */
    Long.UONE = UONE;

    /**
     * @type {!Long}
     * @inner
     */
    var NEG_ONE = fromInt(-1);

    /**
     * Signed negative one.
     * @type {!Long}
     * @expose
     */
    Long.NEG_ONE = NEG_ONE;

    /**
     * @type {!Long}
     * @inner
     */
    var MAX_VALUE = fromBits(0xFFFFFFFF | 0, 0x7FFFFFFF | 0, false);

    /**
     * Maximum signed value.
     * @type {!Long}
     * @expose
     */
    Long.MAX_VALUE = MAX_VALUE;

    /**
     * @type {!Long}
     * @inner
     */
    var MAX_UNSIGNED_VALUE = fromBits(0xFFFFFFFF | 0, 0xFFFFFFFF | 0, true);

    /**
     * Maximum unsigned value.
     * @type {!Long}
     * @expose
     */
    Long.MAX_UNSIGNED_VALUE = MAX_UNSIGNED_VALUE;

    /**
     * @type {!Long}
     * @inner
     */
    var MIN_VALUE = fromBits(0, 0x80000000 | 0, false);

    /**
     * Minimum signed value.
     * @type {!Long}
     * @expose
     */
    Long.MIN_VALUE = MIN_VALUE;

    /**
     * @alias Long.prototype
     * @inner
     */
    var LongPrototype = Long.prototype;

    /**
     * Converts the Long to a 32 bit integer, assuming it is a 32 bit integer.
     * @returns {number}
     * @expose
     */
    LongPrototype.toInt = function toInt() {
        return this.unsigned ? this.low >>> 0 : this.low;
    };

    /**
     * Converts the Long to a the nearest floating-point representation of this value (double, 53 bit mantissa).
     * @returns {number}
     * @expose
     */
    LongPrototype.toNumber = function toNumber() {
        if (this.unsigned)
            return ((this.high >>> 0) * TWO_PWR_32_DBL) + (this.low >>> 0);
        return this.high * TWO_PWR_32_DBL + (this.low >>> 0);
    };

    /**
     * Converts the Long to a string written in the specified radix.
     * @param {number=} radix Radix (2-36), defaults to 10
     * @returns {string}
     * @override
     * @throws {RangeError} If `radix` is out of range
     * @expose
     */
    LongPrototype.toString = function toString(radix) {
        radix = radix || 10;
        if (radix < 2 || 36 < radix)
            throw RangeError('radix');
        if (this.isZero())
            return '0';
        if (this.isNegative()) { // Unsigned Longs are never negative
            if (this.eq(MIN_VALUE)) {
                // We need to change the Long value before it can be negated, so we remove
                // the bottom-most digit in this base and then recurse to do the rest.
                var radixLong = fromNumber(radix),
                    div = this.div(radixLong),
                    rem1 = div.mul(radixLong).sub(this);
                return div.toString(radix) + rem1.toInt().toString(radix);
            } else
                return '-' + this.neg().toString(radix);
        }

        // Do several (6) digits each time through the loop, so as to
        // minimize the calls to the very expensive emulated div.
        var radixToPower = fromNumber(pow_dbl(radix, 6), this.unsigned),
            rem = this;
        var result = '';
        while (true) {
            var remDiv = rem.div(radixToPower),
                intval = rem.sub(remDiv.mul(radixToPower)).toInt() >>> 0,
                digits = intval.toString(radix);
            rem = remDiv;
            if (rem.isZero())
                return digits + result;
            else {
                while (digits.length < 6)
                    digits = '0' + digits;
                result = '' + digits + result;
            }
        }
    };

    /**
     * Gets the high 32 bits as a signed integer.
     * @returns {number} Signed high bits
     * @expose
     */
    LongPrototype.getHighBits = function getHighBits() {
        return this.high;
    };

    /**
     * Gets the high 32 bits as an unsigned integer.
     * @returns {number} Unsigned high bits
     * @expose
     */
    LongPrototype.getHighBitsUnsigned = function getHighBitsUnsigned() {
        return this.high >>> 0;
    };

    /**
     * Gets the low 32 bits as a signed integer.
     * @returns {number} Signed low bits
     * @expose
     */
    LongPrototype.getLowBits = function getLowBits() {
        return this.low;
    };

    /**
     * Gets the low 32 bits as an unsigned integer.
     * @returns {number} Unsigned low bits
     * @expose
     */
    LongPrototype.getLowBitsUnsigned = function getLowBitsUnsigned() {
        return this.low >>> 0;
    };

    /**
     * Gets the number of bits needed to represent the absolute value of this Long.
     * @returns {number}
     * @expose
     */
    LongPrototype.getNumBitsAbs = function getNumBitsAbs() {
        if (this.isNegative()) // Unsigned Longs are never negative
            return this.eq(MIN_VALUE) ? 64 : this.neg().getNumBitsAbs();
        var val = this.high != 0 ? this.high : this.low;
        for (var bit = 31; bit > 0; bit--)
            if ((val & (1 << bit)) != 0)
                break;
        return this.high != 0 ? bit + 33 : bit + 1;
    };

    /**
     * Tests if this Long's value equals zero.
     * @returns {boolean}
     * @expose
     */
    LongPrototype.isZero = function isZero() {
        return this.high === 0 && this.low === 0;
    };

    /**
     * Tests if this Long's value is negative.
     * @returns {boolean}
     * @expose
     */
    LongPrototype.isNegative = function isNegative() {
        return !this.unsigned && this.high < 0;
    };

    /**
     * Tests if this Long's value is positive.
     * @returns {boolean}
     * @expose
     */
    LongPrototype.isPositive = function isPositive() {
        return this.unsigned || this.high >= 0;
    };

    /**
     * Tests if this Long's value is odd.
     * @returns {boolean}
     * @expose
     */
    LongPrototype.isOdd = function isOdd() {
        return (this.low & 1) === 1;
    };

    /**
     * Tests if this Long's value is even.
     * @returns {boolean}
     * @expose
     */
    LongPrototype.isEven = function isEven() {
        return (this.low & 1) === 0;
    };

    /**
     * Tests if this Long's value equals the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.equals = function equals(other) {
        if (!isLong(other))
            other = fromValue(other);
        if (this.unsigned !== other.unsigned && (this.high >>> 31) === 1 && (other.high >>> 31) === 1)
            return false;
        return this.high === other.high && this.low === other.low;
    };

    /**
     * Tests if this Long's value equals the specified's. This is an alias of {@link Long#equals}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.eq = LongPrototype.equals;

    /**
     * Tests if this Long's value differs from the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.notEquals = function notEquals(other) {
        return !this.eq(/* validates */ other);
    };

    /**
     * Tests if this Long's value differs from the specified's. This is an alias of {@link Long#notEquals}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.neq = LongPrototype.notEquals;

    /**
     * Tests if this Long's value is less than the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.lessThan = function lessThan(other) {
        return this.comp(/* validates */ other) < 0;
    };

    /**
     * Tests if this Long's value is less than the specified's. This is an alias of {@link Long#lessThan}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.lt = LongPrototype.lessThan;

    /**
     * Tests if this Long's value is less than or equal the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.lessThanOrEqual = function lessThanOrEqual(other) {
        return this.comp(/* validates */ other) <= 0;
    };

    /**
     * Tests if this Long's value is less than or equal the specified's. This is an alias of {@link Long#lessThanOrEqual}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.lte = LongPrototype.lessThanOrEqual;

    /**
     * Tests if this Long's value is greater than the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.greaterThan = function greaterThan(other) {
        return this.comp(/* validates */ other) > 0;
    };

    /**
     * Tests if this Long's value is greater than the specified's. This is an alias of {@link Long#greaterThan}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.gt = LongPrototype.greaterThan;

    /**
     * Tests if this Long's value is greater than or equal the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.greaterThanOrEqual = function greaterThanOrEqual(other) {
        return this.comp(/* validates */ other) >= 0;
    };

    /**
     * Tests if this Long's value is greater than or equal the specified's. This is an alias of {@link Long#greaterThanOrEqual}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {boolean}
     * @expose
     */
    LongPrototype.gte = LongPrototype.greaterThanOrEqual;

    /**
     * Compares this Long's value with the specified's.
     * @param {!Long|number|string} other Other value
     * @returns {number} 0 if they are the same, 1 if the this is greater and -1
     *  if the given one is greater
     * @expose
     */
    LongPrototype.compare = function compare(other) {
        if (!isLong(other))
            other = fromValue(other);
        if (this.eq(other))
            return 0;
        var thisNeg = this.isNegative(),
            otherNeg = other.isNegative();
        if (thisNeg && !otherNeg)
            return -1;
        if (!thisNeg && otherNeg)
            return 1;
        // At this point the sign bits are the same
        if (!this.unsigned)
            return this.sub(other).isNegative() ? -1 : 1;
        // Both are positive if at least one is unsigned
        return (other.high >>> 0) > (this.high >>> 0) || (other.high === this.high && (other.low >>> 0) > (this.low >>> 0)) ? -1 : 1;
    };

    /**
     * Compares this Long's value with the specified's. This is an alias of {@link Long#compare}.
     * @function
     * @param {!Long|number|string} other Other value
     * @returns {number} 0 if they are the same, 1 if the this is greater and -1
     *  if the given one is greater
     * @expose
     */
    LongPrototype.comp = LongPrototype.compare;

    /**
     * Negates this Long's value.
     * @returns {!Long} Negated Long
     * @expose
     */
    LongPrototype.negate = function negate() {
        if (!this.unsigned && this.eq(MIN_VALUE))
            return MIN_VALUE;
        return this.not().add(ONE);
    };

    /**
     * Negates this Long's value. This is an alias of {@link Long#negate}.
     * @function
     * @returns {!Long} Negated Long
     * @expose
     */
    LongPrototype.neg = LongPrototype.negate;

    /**
     * Returns the sum of this and the specified Long.
     * @param {!Long|number|string} addend Addend
     * @returns {!Long} Sum
     * @expose
     */
    LongPrototype.add = function add(addend) {
        if (!isLong(addend))
            addend = fromValue(addend);

        // Divide each number into 4 chunks of 16 bits, and then sum the chunks.

        var a48 = this.high >>> 16;
        var a32 = this.high & 0xFFFF;
        var a16 = this.low >>> 16;
        var a00 = this.low & 0xFFFF;

        var b48 = addend.high >>> 16;
        var b32 = addend.high & 0xFFFF;
        var b16 = addend.low >>> 16;
        var b00 = addend.low & 0xFFFF;

        var c48 = 0, c32 = 0, c16 = 0, c00 = 0;
        c00 += a00 + b00;
        c16 += c00 >>> 16;
        c00 &= 0xFFFF;
        c16 += a16 + b16;
        c32 += c16 >>> 16;
        c16 &= 0xFFFF;
        c32 += a32 + b32;
        c48 += c32 >>> 16;
        c32 &= 0xFFFF;
        c48 += a48 + b48;
        c48 &= 0xFFFF;
        return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned);
    };

    /**
     * Returns the difference of this and the specified Long.
     * @param {!Long|number|string} subtrahend Subtrahend
     * @returns {!Long} Difference
     * @expose
     */
    LongPrototype.subtract = function subtract(subtrahend) {
        if (!isLong(subtrahend))
            subtrahend = fromValue(subtrahend);
        return this.add(subtrahend.neg());
    };

    /**
     * Returns the difference of this and the specified Long. This is an alias of {@link Long#subtract}.
     * @function
     * @param {!Long|number|string} subtrahend Subtrahend
     * @returns {!Long} Difference
     * @expose
     */
    LongPrototype.sub = LongPrototype.subtract;

    /**
     * Returns the product of this and the specified Long.
     * @param {!Long|number|string} multiplier Multiplier
     * @returns {!Long} Product
     * @expose
     */
    LongPrototype.multiply = function multiply(multiplier) {
        if (this.isZero())
            return ZERO;
        if (!isLong(multiplier))
            multiplier = fromValue(multiplier);
        if (multiplier.isZero())
            return ZERO;
        if (this.eq(MIN_VALUE))
            return multiplier.isOdd() ? MIN_VALUE : ZERO;
        if (multiplier.eq(MIN_VALUE))
            return this.isOdd() ? MIN_VALUE : ZERO;

        if (this.isNegative()) {
            if (multiplier.isNegative())
                return this.neg().mul(multiplier.neg());
            else
                return this.neg().mul(multiplier).neg();
        } else if (multiplier.isNegative())
            return this.mul(multiplier.neg()).neg();

        // If both longs are small, use float multiplication
        if (this.lt(TWO_PWR_24) && multiplier.lt(TWO_PWR_24))
            return fromNumber(this.toNumber() * multiplier.toNumber(), this.unsigned);

        // Divide each long into 4 chunks of 16 bits, and then add up 4x4 products.
        // We can skip products that would overflow.

        var a48 = this.high >>> 16;
        var a32 = this.high & 0xFFFF;
        var a16 = this.low >>> 16;
        var a00 = this.low & 0xFFFF;

        var b48 = multiplier.high >>> 16;
        var b32 = multiplier.high & 0xFFFF;
        var b16 = multiplier.low >>> 16;
        var b00 = multiplier.low & 0xFFFF;

        var c48 = 0, c32 = 0, c16 = 0, c00 = 0;
        c00 += a00 * b00;
        c16 += c00 >>> 16;
        c00 &= 0xFFFF;
        c16 += a16 * b00;
        c32 += c16 >>> 16;
        c16 &= 0xFFFF;
        c16 += a00 * b16;
        c32 += c16 >>> 16;
        c16 &= 0xFFFF;
        c32 += a32 * b00;
        c48 += c32 >>> 16;
        c32 &= 0xFFFF;
        c32 += a16 * b16;
        c48 += c32 >>> 16;
        c32 &= 0xFFFF;
        c32 += a00 * b32;
        c48 += c32 >>> 16;
        c32 &= 0xFFFF;
        c48 += a48 * b00 + a32 * b16 + a16 * b32 + a00 * b48;
        c48 &= 0xFFFF;
        return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned);
    };

    /**
     * Returns the product of this and the specified Long. This is an alias of {@link Long#multiply}.
     * @function
     * @param {!Long|number|string} multiplier Multiplier
     * @returns {!Long} Product
     * @expose
     */
    LongPrototype.mul = LongPrototype.multiply;

    /**
     * Returns this Long divided by the specified. The result is signed if this Long is signed or
     *  unsigned if this Long is unsigned.
     * @param {!Long|number|string} divisor Divisor
     * @returns {!Long} Quotient
     * @expose
     */
    LongPrototype.divide = function divide(divisor) {
        if (!isLong(divisor))
            divisor = fromValue(divisor);
        if (divisor.isZero())
            throw Error('division by zero');
        if (this.isZero())
            return this.unsigned ? UZERO : ZERO;
        var approx, rem, res;
        if (!this.unsigned) {
            // This section is only relevant for signed longs and is derived from the
            // closure library as a whole.
            if (this.eq(MIN_VALUE)) {
                if (divisor.eq(ONE) || divisor.eq(NEG_ONE))
                    return MIN_VALUE;  // recall that -MIN_VALUE == MIN_VALUE
                else if (divisor.eq(MIN_VALUE))
                    return ONE;
                else {
                    // At this point, we have |other| >= 2, so |this/other| < |MIN_VALUE|.
                    var halfThis = this.shr(1);
                    approx = halfThis.div(divisor).shl(1);
                    if (approx.eq(ZERO)) {
                        return divisor.isNegative() ? ONE : NEG_ONE;
                    } else {
                        rem = this.sub(divisor.mul(approx));
                        res = approx.add(rem.div(divisor));
                        return res;
                    }
                }
            } else if (divisor.eq(MIN_VALUE))
                return this.unsigned ? UZERO : ZERO;
            if (this.isNegative()) {
                if (divisor.isNegative())
                    return this.neg().div(divisor.neg());
                return this.neg().div(divisor).neg();
            } else if (divisor.isNegative())
                return this.div(divisor.neg()).neg();
            res = ZERO;
        } else {
            // The algorithm below has not been made for unsigned longs. It's therefore
            // required to take special care of the MSB prior to running it.
            if (!divisor.unsigned)
                divisor = divisor.toUnsigned();
            if (divisor.gt(this))
                return UZERO;
            if (divisor.gt(this.shru(1))) // 15 >>> 1 = 7 ; with divisor = 8 ; true
                return UONE;
            res = UZERO;
        }

        // Repeat the following until the remainder is less than other:  find a
        // floating-point that approximates remainder / other *from below*, add this
        // into the result, and subtract it from the remainder.  It is critical that
        // the approximate value is less than or equal to the real value so that the
        // remainder never becomes negative.
        rem = this;
        while (rem.gte(divisor)) {
            // Approximate the result of division. This may be a little greater or
            // smaller than the actual value.
            approx = Math.max(1, Math.floor(rem.toNumber() / divisor.toNumber()));

            // We will tweak the approximate result by changing it in the 48-th digit or
            // the smallest non-fractional digit, whichever is larger.
            var log2 = Math.ceil(Math.log(approx) / Math.LN2),
                delta = (log2 <= 48) ? 1 : pow_dbl(2, log2 - 48),

                // Decrease the approximation until it is smaller than the remainder.  Note
                // that if it is too large, the product overflows and is negative.
                approxRes = fromNumber(approx),
                approxRem = approxRes.mul(divisor);
            while (approxRem.isNegative() || approxRem.gt(rem)) {
                approx -= delta;
                approxRes = fromNumber(approx, this.unsigned);
                approxRem = approxRes.mul(divisor);
            }

            // We know the answer can't be zero... and actually, zero would cause
            // infinite recursion since we would make no progress.
            if (approxRes.isZero())
                approxRes = ONE;

            res = res.add(approxRes);
            rem = rem.sub(approxRem);
        }
        return res;
    };

    /**
     * Returns this Long divided by the specified. This is an alias of {@link Long#divide}.
     * @function
     * @param {!Long|number|string} divisor Divisor
     * @returns {!Long} Quotient
     * @expose
     */
    LongPrototype.div = LongPrototype.divide;

    /**
     * Returns this Long modulo the specified.
     * @param {!Long|number|string} divisor Divisor
     * @returns {!Long} Remainder
     * @expose
     */
    LongPrototype.modulo = function modulo(divisor) {
        if (!isLong(divisor))
            divisor = fromValue(divisor);
        return this.sub(this.div(divisor).mul(divisor));
    };

    /**
     * Returns this Long modulo the specified. This is an alias of {@link Long#modulo}.
     * @function
     * @param {!Long|number|string} divisor Divisor
     * @returns {!Long} Remainder
     * @expose
     */
    LongPrototype.mod = LongPrototype.modulo;

    /**
     * Returns the bitwise NOT of this Long.
     * @returns {!Long}
     * @expose
     */
    LongPrototype.not = function not() {
        return fromBits(~this.low, ~this.high, this.unsigned);
    };

    /**
     * Returns the bitwise AND of this Long and the specified.
     * @param {!Long|number|string} other Other Long
     * @returns {!Long}
     * @expose
     */
    LongPrototype.and = function and(other) {
        if (!isLong(other))
            other = fromValue(other);
        return fromBits(this.low & other.low, this.high & other.high, this.unsigned);
    };

    /**
     * Returns the bitwise OR of this Long and the specified.
     * @param {!Long|number|string} other Other Long
     * @returns {!Long}
     * @expose
     */
    LongPrototype.or = function or(other) {
        if (!isLong(other))
            other = fromValue(other);
        return fromBits(this.low | other.low, this.high | other.high, this.unsigned);
    };

    /**
     * Returns the bitwise XOR of this Long and the given one.
     * @param {!Long|number|string} other Other Long
     * @returns {!Long}
     * @expose
     */
    LongPrototype.xor = function xor(other) {
        if (!isLong(other))
            other = fromValue(other);
        return fromBits(this.low ^ other.low, this.high ^ other.high, this.unsigned);
    };

    /**
     * Returns this Long with bits shifted to the left by the given amount.
     * @param {number|!Long} numBits Number of bits
     * @returns {!Long} Shifted Long
     * @expose
     */
    LongPrototype.shiftLeft = function shiftLeft(numBits) {
        if (isLong(numBits))
            numBits = numBits.toInt();
        if ((numBits &= 63) === 0)
            return this;
        else if (numBits < 32)
            return fromBits(this.low << numBits, (this.high << numBits) | (this.low >>> (32 - numBits)), this.unsigned);
        else
            return fromBits(0, this.low << (numBits - 32), this.unsigned);
    };

    /**
     * Returns this Long with bits shifted to the left by the given amount. This is an alias of {@link Long#shiftLeft}.
     * @function
     * @param {number|!Long} numBits Number of bits
     * @returns {!Long} Shifted Long
     * @expose
     */
    LongPrototype.shl = LongPrototype.shiftLeft;

    /**
     * Returns this Long with bits arithmetically shifted to the right by the given amount.
     * @param {number|!Long} numBits Number of bits
     * @returns {!Long} Shifted Long
     * @expose
     */
    LongPrototype.shiftRight = function shiftRight(numBits) {
        if (isLong(numBits))
            numBits = numBits.toInt();
        if ((numBits &= 63) === 0)
            return this;
        else if (numBits < 32)
            return fromBits((this.low >>> numBits) | (this.high << (32 - numBits)), this.high >> numBits, this.unsigned);
        else
            return fromBits(this.high >> (numBits - 32), this.high >= 0 ? 0 : -1, this.unsigned);
    };

    /**
     * Returns this Long with bits arithmetically shifted to the right by the given amount. This is an alias of {@link Long#shiftRight}.
     * @function
     * @param {number|!Long} numBits Number of bits
     * @returns {!Long} Shifted Long
     * @expose
     */
    LongPrototype.shr = LongPrototype.shiftRight;

    /**
     * Returns this Long with bits logically shifted to the right by the given amount.
     * @param {number|!Long} numBits Number of bits
     * @returns {!Long} Shifted Long
     * @expose
     */
    LongPrototype.shiftRightUnsigned = function shiftRightUnsigned(numBits) {
        if (isLong(numBits))
            numBits = numBits.toInt();
        numBits &= 63;
        if (numBits === 0)
            return this;
        else {
            var high = this.high;
            if (numBits < 32) {
                var low = this.low;
                return fromBits((low >>> numBits) | (high << (32 - numBits)), high >>> numBits, this.unsigned);
            } else if (numBits === 32)
                return fromBits(high, 0, this.unsigned);
            else
                return fromBits(high >>> (numBits - 32), 0, this.unsigned);
        }
    };

    /**
     * Returns this Long with bits logically shifted to the right by the given amount. This is an alias of {@link Long#shiftRightUnsigned}.
     * @function
     * @param {number|!Long} numBits Number of bits
     * @returns {!Long} Shifted Long
     * @expose
     */
    LongPrototype.shru = LongPrototype.shiftRightUnsigned;

    /**
     * Converts this Long to signed.
     * @returns {!Long} Signed long
     * @expose
     */
    LongPrototype.toSigned = function toSigned() {
        if (!this.unsigned)
            return this;
        return fromBits(this.low, this.high, false);
    };

    /**
     * Converts this Long to unsigned.
     * @returns {!Long} Unsigned long
     * @expose
     */
    LongPrototype.toUnsigned = function toUnsigned() {
        if (this.unsigned)
            return this;
        return fromBits(this.low, this.high, true);
    };

    window.Long = Long;
})();

/*!
 * Pittypat Web 前端核心 JavaScript 库。
 * 扩展基础类型和 jQuery，实现 Pittypat 的基础前端功能。
 * 
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */
(function ($, $doc) {
    'use strict';

    var $body = $('body'),
        $$ = null,
        // 在 user.load、user.login、user.logout 后被执行的函数
        invokeUserHandlers = null;

    // 扩展基础类型
    (function () {
        Array.prototype.copyWithin = Array.prototype.copyWithin || function (target, start /*, end*/) {
            // Steps 1-2.
            if (this == null) {
                throw new TypeError('this is null or not defined');
            }

            var O = Object(this);

            // Steps 3-5.
            var len = O.length >>> 0;

            // Steps 6-8.
            var relativeTarget = target >> 0;

            var to = relativeTarget < 0 ?
                Math.max(len + relativeTarget, 0) :
                Math.min(relativeTarget, len);

            // Steps 9-11.
            var relativeStart = start >> 0;

            var from = relativeStart < 0 ?
                Math.max(len + relativeStart, 0) :
                Math.min(relativeStart, len);

            // Steps 12-14.
            var end = arguments[2];
            var relativeEnd = end === undefined ? len : end >> 0;

            var final = relativeEnd < 0 ?
                Math.max(len + relativeEnd, 0) :
                Math.min(relativeEnd, len);

            // Step 15.
            var count = Math.min(final - from, len - to);

            // Steps 16-17.
            var direction = 1;

            if (from < to && to < (from + count)) {
                direction = -1;
                from += count - 1;
                to += count - 1;
            }

            // Step 18.
            while (count > 0) {
                if (from in O) {
                    O[to] = O[from];
                } else {
                    delete O[to];
                }

                from += direction;
                to += direction;
                count--;
            }

            // Step 19.
            return O;
        };

        Array.prototype.each = Array.prototype.forEach;

        Array.prototype.fill = Array.prototype.fill || function (value) {
            // Steps 1-2.
            if (this == null) {
                throw new TypeError('this is null or not defined');
            }

            var O = Object(this);

            // Steps 3-5.
            var len = O.length >>> 0;

            // Steps 6-7.
            var start = arguments[1];
            var relativeStart = start >> 0;

            // Step 8.
            var k = relativeStart < 0 ?
                Math.max(len + relativeStart, 0) :
                Math.min(relativeStart, len);

            // Steps 9-10.
            var end = arguments[2];
            var relativeEnd = end === undefined ?
                len : end >> 0;

            // Step 11.
            var final = relativeEnd < 0 ?
                Math.max(len + relativeEnd, 0) :
                Math.min(relativeEnd, len);

            // Step 12.
            while (k < final) {
                O[k] = value;
                k++;
            }

            // Step 13.
            return O;
        };

        Array.prototype.find = Array.prototype.find || function (predicate) {
            if (typeof predicate !== 'function') {
                return undefined;
            }

            var list = Object(this);
            var length = list.length >>> 0;
            var thisArg = arguments[1];
            var value;

            for (var i = 0; i < length; i++) {
                value = list[i];
                if (predicate.call(thisArg, value, i, list)) {
                    return value;
                }
            }
            return undefined;
        };

        Array.prototype.findIndex = Array.prototype.findIndex || function (predicate) {
            if (typeof predicate !== 'function') {
                return -1;
            }

            var list = Object(this);
            var length = list.length >>> 0;
            var thisArg = arguments[1];
            var value;

            for (var i = 0; i < length; i++) {
                value = list[i];
                if (predicate.call(thisArg, value, i, list)) {
                    return i;
                }
            }

            return -1;
        };

        Array.prototype.includes = Array.prototype.includes || function (searchElement, fromIndex) {
            // 1. Let O be ? ToObject(this value).
            if (this == null) {
                throw new TypeError('"this" is null or not defined');
            }

            var o = Object(this);

            // 2. Let len be ? ToLength(? Get(O, "length")).
            var len = o.length >>> 0;

            // 3. If len is 0, return false.
            if (len === 0) {
                return false;
            }

            // 4. Let n be ? ToInteger(fromIndex).
            //    (If fromIndex is undefined, this step produces the value 0.)
            var n = fromIndex | 0;

            // 5. If n ≥ 0, then
            //  a. Let k be n.
            // 6. Else n < 0,
            //  a. Let k be len + n.
            //  b. If k < 0, let k be 0.
            var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);

            function sameValueZero(x, y) {
                return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
            }

            // 7. Repeat, while k < len
            while (k < len) {
                // a. Let elementK be the result of ? Get(O, ! ToString(k)).
                // b. If SameValueZero(searchElement, elementK) is true, return true.
                // c. Increase k by 1. 
                if (sameValueZero(o[k], searchElement)) {
                    return true;
                }
                k++;
            }

            // 8. Return false
            return false;
        };

        Array.prototype.max = function () {
            return Math.max.apply(null, this);
        };

        Array.prototype.min = function () {
            return Math.min.apply(null, this);
        };

        // 将指定的日期加上指定的天数
        Date.prototype.addDays = function (days) {
            var date = new Date(this.getFullYear(), this.getMonth(), this.getDate());
            date.setDate(date.getDate() + days);
            return date;
        };

        Date.prototype.format = function (precision, sp, sundayFirst) {
            var me = this;
            sp = sp || '/';

            if (!precision) {
                return $$.util.fmt.second(me, sp);
            }

            switch (precision.toString().toLowerCase()) {
                case 'year':
                    return me.getFullYear().toString();
                case 'month':
                    return me.getFullYear().toString() + sp + (me.getMonth() + 1).toString().padLeft('0', 2);
                case 'week':
                    return me.getFullYear().toString() + sp + me.getWeek(false, sundayFirst).toString().padLeft('0', 2);
                case 'day':
                    return $$.util.fmt.day($$.util.time.to(me), sp);
                case 'hour':
                    return $$.util.fmt.hour($$.util.time.to(me), sp);
                case 'minute':
                    return $$.util.fmt.minute($$.util.time.to(me), sp);
                default:
                    return $$.util.fmt.second($$.util.time.to(me), sp);
            }
        };

        // 返回从 1 开始的月份值
        Date.prototype.getExactMonth = function () {
            return this.getMonth() + 1;
        };

        // 获取以年份开始的完整月份（1-12）
        Date.prototype.getFullMonth = function () {
            return this.getFullYear() * 100 + this.getMonth() + 1;
        };

        // 获取以年份开始的完整周序号
        Date.prototype.getFullWeek = function (sundayFirst) {
            return this.getWeek(true, sundayFirst);
        };

        // 获取指定日期在当年中处于第几周
        Date.prototype.getWeek = function (fullWeek, sundayFirst) {
            var me = this, y = this.getFullYear();
            // 当前年份的第一天
            var firstDay = new Date(y, 0, 1);
            // 当前年份第一天是周几
            var offset = firstDay.getDay();
            if (!sundayFirst) {
                // 周一是第一天
                offset = (offset || 7) - 1;
            }

            if (offset > 0) {
                // 第一天总是周一或周日
                firstDay = firstDay.addDays(7 - offset);
            }

            if (me < firstDay) {
                return 0;
            }

            if (me == firstDay) {
                return 1;
            }

            var w = 0, day = me.getDay();
            if (!sundayFirst) {
                day = (day || 7) - 1;
            }

            w = Math.floor((me.addDays(offset - day) - firstDay) / 86400000 / 7 + 1);

            return fullWeek ? (y * 100 + w) : w;
        };

        // 获取指定日期所在周的第一天和最后一天
        Date.prototype.weekDates = function (sundayFirst) {
            var offset = this.getDay();
            if (!sundayFirst) {
                // 周一是第一天
                offset = (offset || 7) - 1;
            }

            var firstDay = this.addDays(-offset);

            return {
                firstDay: firstDay,
                lastDay: firstDay.addDays(6)
            };
        };

        Math.log10 = Math.log10 || function (x) {
            return Math.log(x) / Math.LN10;
        };

        Math.log2 = Math.log2 || function (x) {
            return Math.log(x) * Math.LOG2E;
        };

        Number.isNaN = Number.isNaN || function (value) {
            return typeof value === 'number' && isNaN(value);
        }

        if (!Number.MAX_SAFE_INTEGER) {
            Number.MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
        }

        if (!Number.MIN_SAFE_INTEGER) {
            Number.MIN_SAFE_INTEGER = -(Math.pow(2, 53) - 1);
        }

        Number.isSafeInteger = Number.isSafeInteger || function (value) {
            return Number.isInteger(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER;
        };

        String.prototype.charPointAt = String.prototype.charPointAt || function (position) {
            if (this == null) {
                throw TypeError();
            }
            var string = String(this);
            var size = string.length;
            // `ToInteger`
            var index = position ? Number(position) : 0;
            if (index != index) { // better `isNaN`
                index = 0;
            }
            // Account for out-of-bounds indices:
            if (index < 0 || index >= size) {
                return undefined;
            }
            // Get the first code unit
            var first = string.charCodeAt(index);
            var second;
            if ( // check if it’s the start of a surrogate pair
                first >= 0xD800 && first <= 0xDBFF && // high surrogate
                size > index + 1 // there is a next code unit
            ) {
                second = string.charCodeAt(index + 1);
                if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
                    // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
                    return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
                }
            }
            return first;
        };

        String.prototype.endsWith = String.prototype.endsWith || function (s, pos) {
            var ss = this.toString();
            if (typeof pos !== 'number' || !isFinite(pos) || Math.floor(pos) !== pos || pos > subjectString.length) {
                pos = ss.length;
            }

            pos -= s.length;
            var i = ss.indexOf(s, pos);
            return i !== -1 && i === pos;
        };

        String.prototype.includes = String.prototype.includes || function (search, start) {
            if (typeof start !== 'number') {
                start = 0;
            }

            if (start + search.length > this.length) {
                return false;
            } else {
                return this.indexOf(search, start) !== -1;
            }
        };

        String.prototype.left = function (len) {
            return this.length > len ? this.substr(0, len) : this;
        };

        String.prototype.padEnd = String.prototype.padEnd || function (targetLength, padString) {
            targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
            padString = String(padString || ' ');
            if (this.length > targetLength) {
                return String(this);
            }
            else {
                targetLength = targetLength - this.length;
                if (targetLength > padString.length) {
                    padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
                }
                return String(this) + padString.slice(0, targetLength);
            }
        };

        String.prototype.padLeft = function (ch, len) {
            return this.padStart(len, ch);
        };

        String.prototype.padRight = function (ch, len) {
            return this.padEnd(len, ch);
        };

        String.prototype.padStart = String.prototype.padStart || function (targetLength, padString) {
            targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
            padString = String(padString || ' ');
            if (this.length > targetLength) {
                return String(this);
            }
            else {
                targetLength = targetLength - this.length;
                if (targetLength > padString.length) {
                    padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
                }
                return padString.slice(0, targetLength) + String(this);
            }
        };

        String.prototype.repeat = String.prototype.repeat || function (count) {
            var str = '' + this;
            count = +count;
            if (count != count) {
                count = 0;
            }
            if (count < 0) {
                throw new RangeError('repeat count must be non-negative');
            }
            if (count == Infinity) {
                throw new RangeError('repeat count must be less than infinity');
            }
            count = Math.floor(count);
            if (str.length == 0 || count == 0) {
                return '';
            }
            // Ensuring count is a 31-bit integer allows us to heavily optimize the
            // main part. But anyway, most current (August 2014) browsers can't handle
            // strings 1 << 28 chars or longer, so:
            if (str.length * count >= 1 << 28) {
                throw new RangeError('repeat count must not overflow maximum string size');
            }
            var rpt = '';
            for (; ;) {
                if ((count & 1) == 1) {
                    rpt += str;
                }
                count >>>= 1;
                if (count == 0) {
                    break;
                }
                str += str;
            }
            // Could we try:
            // return Array(count + 1).join(this);
            return rpt;
        };

        String.prototype.replaceAll = function (s, r) {
            return this.replace(new RegExp(s, 'g'), r);
        };

        String.prototype.right = function (len) {
            return this.length > len ? this.substr(this.length - len, len) : this;
        };

        String.prototype.startsWith = String.prototype.startsWith || function (s, pos) {
            pos = pos || 0;
            return this.substr(pos, s.length) === s;
        };

        String.prototype.trim = function (m) {
            var v = this;
            if (v.length) {
                m = !m ? '' : m.toLowerCase();

                if (m == 'start' || m == 'begin') {
                    v = v.replace(/(^[\s\uFEFF\xA0]+)/g, "");
                } else if (m == 'end') {
                    v = v.replace(/([\s\uFEFF\xA0]+$)/g, "");
                } else if (m == 'all') {
                    v = v.replace(/[\s\uFEFF\xA0]+/g, "");
                } else {
                    v = v.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
                }
            }

            return v;
        };
    })();

    // 扩展 jQuery
    (function () {
        function getCaret(t) {
            var start = 0, end = 0;

            if (typeof t.selectionStart === 'number' && typeof t.selectionEnd === 'number') {
                start = t.selectionStart;
                end = t.selectionEnd;
            } else {
                var nv, rng, trng, len, erng;
                rng = document.selection.createRange();

                if (rng && rng.parentElement() == t) {
                    len = t.value.length;
                    nv = t.value.replace(/\r\n/g, '\n');

                    trng = t.createTextRange();
                    trng.moveToBookmark(rng.getBookmark());

                    erng = t.createTextRange();
                    erng.collapse(false);

                    if (trng.compareEndPoints('StartToEnd', erng) > -1) {
                        start = end = len;
                    } else {
                        start = -trng.moveStart('character', -len);
                        start += nv.slice(0, start).split('\n').length - 1;

                        if (trng.compareEndPoints('EndToEnd', erng) > -1) {
                            end = len;
                        } else {
                            end = -trng.moveEnd('character', -len);
                            end += nv.slice(0, end).split('\n').length - 1;
                        }
                    }
                }
            }

            return {
                start: start,
                end: end
            };
        }

        function intPos(p, min, max) {
            var s = typeof p;

            if (s === 'number') {
                return p < min ? min : (p > max ? max : p);
            }

            if (s === 'string') {
                p = p.toLowerCase();
                if (p == 'start') {
                    return min;
                }

                if (p == 'end') {
                    return max;
                }

                p = parseInt(p, 10);
                return isNaN(p) || p < min ? min : (p > max ? max : p);
            }

            return min;
        }

        function setCaret(t, start, end) {
            try {
                var s = t.value;

                if (typeof start === 'string' && start.toLowerCase() == 'all') {
                    start = 0;
                    end = s.length;
                } else {
                    start = intPos(start, 0, s.length);
                    end = intPos(end, start, s.length);
                }

                if (window.getSelection) {
                    window.getSelection().removeAllRanges();
                }

                if (t.setSelectionRange) {
                    t.setSelectionRange(start, end);
                } else if (t.selectionStart && t.selectionEnd) {
                    t.selectionStart = start;
                    t.selectionEnd = end;
                } else if (t.createTextRange) {
                    var rng = t.createTextRange();
                    rng.collapse(true);
                    rng.moveEnd('character', end);
                    rng.moveStart('character', start);
                    rng.select();
                }
            } catch (e) {
                console.log(e);
            }

            t.focus();
        }

        // 光标位置
        $.fn.caret = function () {
            var v = arguments;
            if (!v.length) {
                return getCaret(this[0]);
            }
            else {
                if (this.is(':visible')) {
                    setCaret(this[0], v[0], v.length > 1 ? v[1] : null);
                }

                return this;
            }
        };

        // 多行文本
        $.fn.multiline = function (s) {
            var me = this;

            if (s === null || typeof s === 'undefined') {
                s = '';
            } else {
                s = typeof s === 'string' ? s : s.toString();
            }

            if (s.length > 0) {
                var v = s.split(/\r\n|\r|\n/g);

                if (v.length == 1) {
                    me.text(v[0]);
                } else {
                    me.empty();
                    var i = 0;
                    for (; i < v.length - 1; ++i) {
                        $('<span></span>').text(v[i]).appendTo(me);
                        me.append('<br />');
                    }
                    $('<span></span>').text(v[i]).appendTo(me);
                }
            } else {
                me.text('');
            }

            return this;
        };

        // 文本或值
        $.fn.tv = function () {
            var me = this,
                b = me.is('input,textarea,select');

            if (arguments.length < 1) {
                return b ? me.val() : me.text();
            } else {
                var s = $$.val(arguments[0]);
                if (typeof s === 'undefined' || s === null) {
                    s = '';
                } else {
                    s = s.toString();
                }

                if (b) {
                    me.val(s);
                } else {
                    me.text(s);
                }

                return me;
            }
        };
        
        // 初始化 MDC
        $.fn.mdcInit = function () {
            this.each(function (i, e) {
                mdc.autoInit(e);
            });

            return this;
        };
    })();

    // 根命名空间
    (function () {
        var USER_HANDLERS = [],
            firstAuthorized = true,
            firstAnonymous = true,
            init = true;

        invokeUserHandlers = function () {
            // args.init：
            // 对 authorized 和 anonymous 同时有效。
            // 首次执行 authorized 或 anonymous 时为 true，
            // 在执行 authorized 或 anonymous 之后设置为 false。
            // 整个页面中只会有一次 init。

            // args.first：
            // 对 authorized 和 anonymous 分别有效。
            // 首次执行 authorized 和 anonymous 时为 true，
            // 在执行 authorized 或 anonymous 之后设置为 false。
            // 整个页面执行 authorized 和 anonymous 时有两次为 true。

            var _user = $$.user.current;

            var _app = null,
                _mod = null,
                _init = init,
                _first = _user ? firstAuthorized : firstAnonymous,
                s = window.location.pathname.toLowerCase();

            init = false;

            if (_user) {
                firstAuthorized = false;
            } else {
                firstAnonymous = false;
            }

            if ($$.web.appPath && s.startsWith($$.web.appPath)) {
                s = s.substring($$.web.appPath.length);
            }

            var v = s.toLowerCase().split('/');
            if (v.length > 4) {
                try {
                    _app = $$.com[v[2]][v[3]];
                    _mod = _app[v[4]];
                } catch (e) {
                    console.log(e);
                    _app = null;
                    _mod = null;
                }
            }

            var args = {
                get user() {
                    // 此处不宜返回 _user，这可能导致 args.user 属性不能反映实际情况
                    return $$.user.current;
                },
                get app() {
                    return _app;
                },
                get mod() {
                    return _mod;
                },
                get init() {
                    return _init;
                },
                get first() {
                    return _first;
                },
                data: null
            };

            if (_user) {
                // 登录用户
                if (_init) {
                    USER_HANDLERS.each(function (h) {
                        args.data = $$.val(h.data, args);
                        if (typeof args.data === 'undefined') {
                            args.data = null;
                        }
                        h.init(args);
                    });

                    USER_HANDLERS.each(function (h) {
                        h.authorized(args);
                    });
                } else {
                    USER_HANDLERS.each(function (h) {
                        args.data = $$.val(h.data, args);
                        if (typeof args.data === 'undefined') {
                            args.data = null;
                        }
                        h.authorized(args);
                    });
                }
            } else {
                // 匿名用户
                if (_init) {
                    USER_HANDLERS.each(function (h) {
                        args.data = $$.val(h.data, args);
                        if (typeof args.data === 'undefined') {
                            args.data = null;
                        }
                        h.init(args);
                    });

                    USER_HANDLERS.each(function (h) {
                        h.anonymous(args);
                    });
                } else {
                    USER_HANDLERS.each(function (h) {
                        args.data = $$.val(h.data, args);
                        if (typeof args.data === 'undefined') {
                            args.data = null;
                        }
                        h.anonymous(args);
                    });
                }
            }
        };

        $$ = window.pittypat = function (handlers) {
            if (handlers) {
                if (typeof handlers === 'function') {
                    USER_HANDLERS.push({
                        init: $.noop,
                        authorized: handlers,
                        anonymous: $$.user.goHome,
                        data: null
                    });
                } else if (typeof handlers === 'object') {
                    USER_HANDLERS.push({
                        init: typeof handlers.init === 'function' ? handlers.init : $.noop,
                        authorized: typeof handlers.authorized === 'function' ? handlers.authorized : $.noop,
                        anonymous: typeof handlers.anonymous === 'function' ? handlers.anonymous : $$.user.goHome,
                        data: typeof handlers.data === 'undefined' ? null : handlers.data
                    });
                }
            }

            return window.pittypat;
        };

        // 如果 v 不是一个函数，则直接返回 v，否则使用指定的参数执行函数，并返回该函数的结果
        $$.val = function (v, args, _this) {
            return typeof v === 'function' ? v.call(_this, args) : v;
        };
    })();

    // 提供一组辅助功能
    (function () {
        // 辅助功能的根
        $$.util = {};

        // 提供与格式化有关的功能
        $$.util.fmt = {
            // 格式化一个服务器返回的日期时间对象，精确到秒
            second: function (t, sp) {
                sp = typeof sp === 'undefined' ? '/' : sp;
                t = t instanceof Date ? $$.util.time.to(t) : t;
                return !t ? '' : (t.year + sp +
                    t.month.toString().padLeft('0', 2) + sp +
                    t.day.toString().padLeft('0', 2) + ' ' +
                    t.hour.toString().padLeft('0', 2) + ':' +
                    t.minute.toString().padLeft('0', 2) + ':' +
                    t.second.toString().padLeft('0', 2));
            },

            // 格式化一个服务器返回的日期时间对象，精确到分钟
            minute: function (t, sp) {
                sp = typeof sp === 'undefined' ? '/' : sp;
                t = t instanceof Date ? $$.util.time.to(t) : t;
                return !t ? '' : (t.year + sp +
                    t.month.toString().padLeft('0', 2) + sp +
                    t.day.toString().padLeft('0', 2) + ' ' +
                    t.hour.toString().padLeft('0', 2) + ':' +
                    t.minute.toString().padLeft('0', 2));
            },

            // 格式化一个服务器返回的日期时间对象，精确到小时
            hour: function (t, sp) {
                sp = typeof sp === 'undefined' ? '/' : sp;
                t = t instanceof Date ? $$.util.time.to(t) : t;
                return !t ? '' : (t.year + sp +
                    t.month.toString().padLeft('0', 2) + sp +
                    t.day.toString().padLeft('0', 2) + ' ' +
                    t.hour.toString().padLeft('0', 2));
            },

            // 格式化一个服务器返回的日期时间对象，精确到日期
            day: function (t, sp) {
                sp = typeof sp === 'undefined' ? '/' : sp;
                t = t instanceof Date ? $$.util.time.to(t) : t;
                return !t ? '' : (t.year + sp +
                    t.month.toString().padLeft('0', 2) + sp +
                    t.day.toString().padLeft('0', 2));
            },

            // 格式化一个服务器返回的时间对象，精确到秒
            timeSecond: function (t) {
                t = t instanceof Date ? $$.util.time.to(t) : t;
                return !t ? '' : (t.hour.toString().padLeft('0', 2) + ':' +
                    t.minute.toString().padLeft('0', 2) + ':' +
                    t.second.toString().padLeft('0', 2));
            },

            // 格式化一个服务器返回的时间对象，精确到分钟
            timeMinute: function (t) {
                t = t instanceof Date ? $$.util.time.to(t) : t;
                return !t ? '' : (t.hour.toString().padLeft('0', 2) + ':' +
                    t.minute.toString().padLeft('0', 2));
            }
        };

        // 与时间相关的函数
        $$.util.time = {
            // 将一个服务器返回的日期时间对象转换为本地 Date 对象
            from: function (o) {
                if (!o) return null;
                if (o instanceof Date && !isNaN(o.valueOf())) return o;
                return new Date(
                    typeof o.year === 'number' ? o.year : new Date().getFullYear(),
                    typeof o.month === 'number' ? (o.month - 1) : 0,
                    typeof o.day === 'number' ? o.day : 1,
                    typeof o.hour === 'number' ? o.hour : 0,
                    typeof o.minute === 'number' ? o.minute : 0,
                    typeof o.second === 'number' ? o.second : 0,
                    typeof o.millisecond === 'number' ? o.millisecond : 0);
            },

            // 将一个本地 Date 对象转换为服务器日期时间对象
            to: function (t) {
                if (!t) return null;
                if (!(t instanceof Date && !isNaN(t.valueOf()))) return t;
                return {
                    year: t.getFullYear(),
                    month: t.getMonth() + 1,
                    day: t.getDate(),
                    hour: t.getHours(),
                    minute: t.getMinutes(),
                    second: t.getSeconds(),
                    millisecond: t.getMilliseconds()
                };
            },

            // 返回一个客户端本地的当前时刻
            now: function () {
                return new Date();
            },

            // 时间的显示名称
            name: function (t, s) {
                var today = $$.util.date.today(),
                    time = t instanceof Date ? t : $$.util.time.from(t),
                    date = new Date(time.getFullYear(), time.getMonth(), time.getDate()),
                    prefix = '';

                // 默认精度为分钟
                s = (s || 'm').toString().toLowerCase();

                if (today.getFullYear() == date.getFullYear()) {
                    // 相同年份
                    if (today.getMonth() == date.getMonth()) {
                        // 相同月份
                        if (today.getDate() == date.getDate()) {
                            prefix = '今天';
                        } else if (today.getDate() == date.getDate() + 1) {
                            prefix = '昨天';
                        } else if (today.getDate() == date.getDate() + 2) {
                            prefix = '前天';
                        } else if (today.getDate() == date.getDate() - 1) {
                            prefix = '明天';
                        } else if (today.getDate() == date.getDate() - 2) {
                            prefix = '后天';
                        } else {
                            prefix = date.getDate().toString().padLeft('0', 2) + '日';
                        }
                    } else {
                        // 不同月份
                        prefix = date.getExactMonth().toString().padLeft('0', 2) + '月' +
                            date.getDate().toString().padLeft('0', 2) + '日';
                    }
                } else {
                    // 不同年份：2017/06/01 12:34
                    prefix = $$.util.fmt.day(date);
                }
                
                switch (s) {
                    case 'h':
                        // 小时
                        return prefix + ' ' + time.getHours().toString().padLeft('0', 2) + ':00';
                    case 's':
                        // 秒
                        return prefix + ' ' + $$.util.fmt.timeSecond(time);
                    default:
                        // 分钟
                        return prefix + ' ' + $$.util.fmt.timeMinute(time);
                }
            },

            // 比较两个日期时间的大小，用于按日期时间升序排序
            compareAsc: function (x, y) {
                if (!x && !y) return 0;
                if (!x) return -1;
                if (!y) return 1;

                return $$.util.time.from(x).valueOf() - $$.util.time.from(y).valueOf();
            },

            // 比较两个日期时间的大小，用于按日期时间降序排序
            compareDesc: function (x, y) {
                return -$$.util.time.compareAsc(x, y);
            }
        };

        // 与日期相关的函数
        $$.util.date = {
            // 将一个服务器返回的日期时间对象转换为本地 Date 对象
            from: function (o) {
                if (!o) return null;
                return new Date(
                    typeof o.year === 'number' ? o.year : new Date().getFullYear(),
                    typeof o.month === 'number' ? (o.month - 1) : 0,
                    typeof o.day === 'number' ? o.day : 1);
            },

            // 将一个本地 Date 对象转换为服务器日期对象，时间部分总是为 0
            to: function (t) {
                if (!t) return null;
                return {
                    year: t.getFullYear(),
                    month: t.getMonth() + 1,
                    day: t.getDate(),
                    hour: 0,
                    minute: 0,
                    second: 0,
                    millisecond: 0
                };
            },

            // 返回一个客户端本地的当前时刻，时间部分总是为 0
            today: function () {
                var t = new Date();
                t.setHours(0, 0, 0, 0);
                return t;
            },

            // 判断指定的年份是否闰年
            isLeapYear: function (year) {
                return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
            },

            // 获取指定月份（1-12）中的天数
            daysInMonth: function (year, month) {
                return [31, ($$.util.date.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
            },

            // 获取指定序号的周的第一天和最后一天的日期
            weekDates: function (yw, sundayFirst) {
                var md = null, sd = null, year = Math.floor(yw / 100), week = yw % 100;
                // 目标年份第一天
                var firstDay = new Date(year, 0, 1);

                var offset = firstDay.getDay();
                if (!sundayFirst) {
                    // 周一是第一天
                    offset = (offset || 7) - 1;
                }

                if (offset > 0) {
                    // 第一天总是周一或周日
                    firstDay = firstDay.addDays(7 - offset);
                }

                if (week != 1) {
                    firstDay.setTime(firstDay.valueOf() + (week - 1) * 86400000 * 7);
                }

                return {
                    firstDay: firstDay,
                    lastDay: firstDay.addDays(6)
                };
            }
        };

        // 与整数相关的函数
        $$.util.int = {
            // 严格地按照十进制解析整数字符串
            parse: function (s) {
                if (/^(\-|\+)?([0-9]+|Infinity)$/.test(s)) {
                    return Number(s);
                }
                return NaN;
            },

            // 返回大于等于 min 小于 max 的一个随机整数
            random: function (min, max) {
                min = Math.ceil(min);
                max = Math.floor(max);
                return Math.floor(Math.random() * (max - min)) + min;
            }
        };

        // 与浮点数相关的函数
        $$.util.float = {
            // 严格地按照十进制解析浮点数字符串
            parse: function (s) {
                if (/^(\-|\+)?([0-9]+(\.[0-9]+)?|Infinity)$/.test(s)) {
                    return Number(s);
                }
                return NaN;
            },

            // 返回大于等于 min 小于 max 的一个随机浮点数
            random: function (min, max) {
                return Math.floor(Math.random() * (max - min)) + min;
            }
        };

        var CHARS64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('');
        function b64_e(i24, p) {
            return CHARS64[i24 >>> 18 & 63] + CHARS64[i24 >>> 12 & 63] + CHARS64[p == 2 ? 64 : i24 >>> 6 & 63] + CHARS64[p ? 64 : i24 & 63];
        }

        function b64_p(s, i) {
            var c = s.charCodeAt(i);
            return c > 64 && c < 91 ? c - 65 : c > 96 && c < 123 ? c - 71 : c > 47 && c < 58 ? c + 4 : c === 43 ? 62 : c === 47 ? 63 : 0;
        }

        function b64_d(b, i24, oi) {
            b[oi++] = i24 >>> 16;
            if (oi < b.length) b[oi++] = (i24 & 0xff00) >>> 8;
            if (oi < b.length) b[oi++] = i24 & 0xff;
            return oi;
        }

        // 与字节数组相关的函数
        $$.util.binary = {
            // 将指定的字节数组转换为 base64 格式
            toBase64: function (b) {
                if (!b || !b.length) return '';

                var s = '';
                var m = b.length % 3, i = 0;
                for (var n = b.length - m; i < n; i += 3) {
                    s = s + b64_e((b[i] << 16) | (b[i + 1] << 8) | b[i + 2]);
                }

                if (m) {
                    s = s + b64_e((b[i] << 16) | (m == 1 ? 0 : (b[i + 1] << 8)), 3 - m);
                }

                return s;
            },

            // 将 base64 转换为字节数组
            fromBase64: function (s) {
                if (!s) return [];
                if (s.length % 4) throw new Error('base64 字符串的长度应该是 4 的整数倍。');
                var n = s.length, p = 0;
                if (s.charCodeAt(n - 1) == 61)++p;
                if (s.charCodeAt(n - 2) == 61)++p;
                var b = new Array((n - p) * 3 + 1 >>> 2), i = 0;
                n = p ? n - 4 : n;
                for (var oi = 0; i < n; i += 4) {
                    oi = b64_d(b, (b64_p(s, i) << 18) | (b64_p(s, i + 1) << 12) | (b64_p(s, i + 2) << 6) | b64_p(s, i + 3), oi);
                }

                if (p) {
                    oi = b64_d(b, (b64_p(s, i) << 18) | (b64_p(s, i + 1) << 12) | (p == 2 ? 0 : (b64_p(s, i + 2) << 6)) | (p ? 0 : b64_p(s, i + 3)), oi);
                }

                return b;
            }
        };

        // 与字符串相关的函数
        $$.util.string = {
            // 按 utf-8 将指定的字符串转换为字节数组。
            toUtf8: function (s) {
                var b = [];

                if (typeof s === 'string' && s.length > 0) {
                    for (var i = 0, c; i < s.length; ++i) {
                        c = s.charCodeAt(i);

                        if (c < 0x80) {
                            b.push(c);
                        } else if (c < 0x800) {
                            b.push(0xc0 | (c >>> 6), 0x80 | (c & 0x3f));
                        } else if (c < 0xd800 || c >= 0xe000) {
                            b.push(0xe0 | (c >>> 12), 0x80 | ((c >>> 6) & 0x3f), 0x80 | (c & 0x3f));
                        } else {
                            c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(++i) & 0x3ff));
                            b.push(0xf0 | (c >>> 18), 0x80 | ((c >>> 12) & 0x3f), 0x80 | ((c >>> 6) & 0x3f), 0x80 | (c & 0x3f));
                        }
                    }
                }

                return b;
            },

            // 按 utf-8 将指定的字节数组转换为字符串。
            fromUtf8: function (b) {
                var s = '';

                if (b && b.length > 0) {
                    for (var i = 0, f = String.fromCharCode; i < b.length; ++i) {
                        if ((b[i] & 0xf0) === 0xf0) {
                            s += f(((b[i] & 0x07) << 18) | ((b[++i] & 0x3f) << 12) | ((b[++i] & 0x3f) << 6) | (b[++i] & 0x3f));
                        } else if ((b[i] & 0xe0) === 0xe0) {
                            s += f(((b[i] & 0x0f) << 12) | ((b[++i] & 0x3f) << 6) | (b[++i] & 0x3f));
                        } else if ((b[i] & 0xc0) === 0xc0) {
                            s += f(((b[i] & 0x1f) << 6) | (b[++i] & 0x3f));
                        } else {
                            s += f(b[i]);
                        }
                    }
                }

                return s;
            },

            // 压缩指定的字符串为字节数组。根据 LZString 调整实现，请参见：http://pieroxy.net/blog/pages/lz-string/index.html。
            compress: function (s) {
                if (!s) return '';
                var i, val,
                    bits = 16,
                    dict = {},
                    dictToCreate = {},
                    ctx_c = '',
                    ctx_wc = '',
                    ctx_w = '',
                    ctx_enlargeIn = 2,
                    ctx_dictSize = 3,
                    ctx_numBits = 2,
                    ctx_data = [],
                    ctx_data_val = 0,
                    ctx_data_position = 0,
                    ii;

                for (ii = 0; ii < s.length; ++ii) {
                    ctx_c = s.charAt(ii);
                    if (!Object.prototype.hasOwnProperty.call(dict, ctx_c)) {
                        dict[ctx_c] = ctx_dictSize++;
                        dictToCreate[ctx_c] = true;
                    }

                    ctx_wc = ctx_w + ctx_c;
                    if (Object.prototype.hasOwnProperty.call(dict, ctx_wc)) {
                        ctx_w = ctx_wc;
                    } else {
                        if (Object.prototype.hasOwnProperty.call(dictToCreate, ctx_w)) {
                            if (ctx_w.charCodeAt(0) < 256) {
                                for (i = 0; i < ctx_numBits; i++) {
                                    ctx_data_val = (ctx_data_val << 1);
                                    if (ctx_data_position == bits - 1) {
                                        ctx_data_position = 0;
                                        ctx_data.push(ctx_data_val);
                                        ctx_data_val = 0;
                                    } else {
                                        ctx_data_position++;
                                    }
                                }
                                val = ctx_w.charCodeAt(0);
                                for (i = 0; i < 8; i++) {
                                    ctx_data_val = (ctx_data_val << 1) | (val & 1);
                                    if (ctx_data_position == bits - 1) {
                                        ctx_data_position = 0;
                                        ctx_data.push(ctx_data_val);
                                        ctx_data_val = 0;
                                    } else {
                                        ctx_data_position++;
                                    }
                                    val = val >> 1;
                                }
                            } else {
                                val = 1;
                                for (i = 0; i < ctx_numBits; i++) {
                                    ctx_data_val = (ctx_data_val << 1) | val;
                                    if (ctx_data_position == bits - 1) {
                                        ctx_data_position = 0;
                                        ctx_data.push(ctx_data_val);
                                        ctx_data_val = 0;
                                    } else {
                                        ctx_data_position++;
                                    }
                                    val = 0;
                                }
                                val = ctx_w.charCodeAt(0);
                                for (i = 0; i < 16; i++) {
                                    ctx_data_val = (ctx_data_val << 1) | (val & 1);
                                    if (ctx_data_position == bits - 1) {
                                        ctx_data_position = 0;
                                        ctx_data.push(ctx_data_val);
                                        ctx_data_val = 0;
                                    } else {
                                        ctx_data_position++;
                                    }
                                    val = val >> 1;
                                }
                            }
                            ctx_enlargeIn--;
                            if (ctx_enlargeIn == 0) {
                                ctx_enlargeIn = Math.pow(2, ctx_numBits);
                                ctx_numBits++;
                            }
                            delete dictToCreate[ctx_w];
                        } else {
                            val = dict[ctx_w];
                            for (i = 0; i < ctx_numBits; i++) {
                                ctx_data_val = (ctx_data_val << 1) | (val & 1);
                                if (ctx_data_position == bits - 1) {
                                    ctx_data_position = 0;
                                    ctx_data.push(ctx_data_val);
                                    ctx_data_val = 0;
                                } else {
                                    ctx_data_position++;
                                }
                                val = val >> 1;
                            }


                        }
                        ctx_enlargeIn--;
                        if (ctx_enlargeIn == 0) {
                            ctx_enlargeIn = Math.pow(2, ctx_numBits);
                            ctx_numBits++;
                        }
                        // Add wc to the dictionary.
                        dict[ctx_wc] = ctx_dictSize++;
                        ctx_w = String(ctx_c);
                    }
                }

                // Output the code for w.
                if (ctx_w !== '') {
                    if (Object.prototype.hasOwnProperty.call(dictToCreate, ctx_w)) {
                        if (ctx_w.charCodeAt(0) < 256) {
                            for (i = 0; i < ctx_numBits; i++) {
                                ctx_data_val = (ctx_data_val << 1);
                                if (ctx_data_position == bits - 1) {
                                    ctx_data_position = 0;
                                    ctx_data.push(ctx_data_val);
                                    ctx_data_val = 0;
                                } else {
                                    ctx_data_position++;
                                }
                            }
                            val = ctx_w.charCodeAt(0);
                            for (i = 0; i < 8; i++) {
                                ctx_data_val = (ctx_data_val << 1) | (val & 1);
                                if (ctx_data_position == bits - 1) {
                                    ctx_data_position = 0;
                                    ctx_data.push(ctx_data_val);
                                    ctx_data_val = 0;
                                } else {
                                    ctx_data_position++;
                                }
                                val = val >> 1;
                            }
                        } else {
                            val = 1;
                            for (i = 0; i < ctx_numBits; i++) {
                                ctx_data_val = (ctx_data_val << 1) | val;
                                if (ctx_data_position == bits - 1) {
                                    ctx_data_position = 0;
                                    ctx_data.push(ctx_data_val);
                                    ctx_data_val = 0;
                                } else {
                                    ctx_data_position++;
                                }
                                val = 0;
                            }
                            val = ctx_w.charCodeAt(0);
                            for (i = 0; i < 16; i++) {
                                ctx_data_val = (ctx_data_val << 1) | (val & 1);
                                if (ctx_data_position == bits - 1) {
                                    ctx_data_position = 0;
                                    ctx_data.push(ctx_data_val);
                                    ctx_data_val = 0;
                                } else {
                                    ctx_data_position++;
                                }
                                val = val >> 1;
                            }
                        }
                        ctx_enlargeIn--;
                        if (ctx_enlargeIn == 0) {
                            ctx_enlargeIn = Math.pow(2, ctx_numBits);
                            ctx_numBits++;
                        }
                        delete dictToCreate[ctx_w];
                    } else {
                        val = dict[ctx_w];
                        for (i = 0; i < ctx_numBits; i++) {
                            ctx_data_val = (ctx_data_val << 1) | (val & 1);
                            if (ctx_data_position == bits - 1) {
                                ctx_data_position = 0;
                                ctx_data.push(ctx_data_val);
                                ctx_data_val = 0;
                            } else {
                                ctx_data_position++;
                            }
                            val = val >> 1;
                        }


                    }
                    ctx_enlargeIn--;
                    if (ctx_enlargeIn == 0) {
                        ctx_enlargeIn = Math.pow(2, ctx_numBits);
                        ctx_numBits++;
                    }
                }

                // Mark the end of the stream
                val = 2;
                for (i = 0; i < ctx_numBits; ++i) {
                    ctx_data_val = (ctx_data_val << 1) | (val & 1);
                    if (ctx_data_position == bits - 1) {
                        ctx_data_position = 0;
                        ctx_data.push(ctx_data_val);
                        ctx_data_val = 0;
                    } else {
                        ctx_data_position++;
                    }
                    val = val >> 1;
                }

                // Flush the last char
                while (true) {
                    ctx_data_val = (ctx_data_val << 1);
                    if (ctx_data_position == bits - 1) {
                        ctx_data.push(ctx_data_val);
                        break;
                    }
                    else ctx_data_position++;
                }

                // 将字符串代码转换为字节数组
                var bytes = new Array(ctx_data.length * 2);
                var code = 0;
                for (var i = 0, len = ctx_data.length; i < len; ++i) {
                    code = ctx_data[i];
                    bytes[i * 2] = code >>> 8;
                    bytes[i * 2 + 1] = code % 256;
                }

                return bytes;
            },

            // 从 Array 解压缩一个字符串，字节数组的长度必须是 2 的整数倍。根据 LZString 调整实现，请参见：http://pieroxy.net/blog/pages/lz-string/index.html。
            decompress: function (b) {
                if (!b || !b.length) return '';

                if ((b.length % 2) != 0) {
                    throw new Error('字节数组的长度必须是 2 的整数倍。');
                }

                // 从字节数组中获取一个字符编码
                function code(i) {
                    return (b[i * 2] << 8) | (b[i * 2 + 1]);
                }

                var dict = [],
                    length = b.length / 2,
                    reset = 32768,
                    next,
                    enlargeIn = 4,
                    dictSize = 4,
                    numBits = 3,
                    entry = '',
                    result = [],
                    i,
                    w,
                    bits, resb, maxpower, power,
                    c,
                    data = { val: code(0), position: reset, index: 1 };

                for (i = 0; i < 3; ++i) {
                    dict[i] = i;
                }

                bits = 0;
                maxpower = Math.pow(2, 2);
                power = 1;
                while (power != maxpower) {
                    resb = data.val & data.position;
                    data.position >>= 1;
                    if (data.position == 0) {
                        data.position = reset;
                        data.val = code(data.index++);
                    }
                    bits |= (resb > 0 ? 1 : 0) * power;
                    power <<= 1;
                }

                switch (next = bits) {
                    case 0:
                        bits = 0;
                        maxpower = Math.pow(2, 8);
                        power = 1;
                        while (power != maxpower) {
                            resb = data.val & data.position;
                            data.position >>= 1;
                            if (data.position == 0) {
                                data.position = reset;
                                data.val = code(data.index++);
                            }
                            bits |= (resb > 0 ? 1 : 0) * power;
                            power <<= 1;
                        }
                        c = String.fromCharCode(bits);
                        break;
                    case 1:
                        bits = 0;
                        maxpower = Math.pow(2, 16);
                        power = 1;
                        while (power != maxpower) {
                            resb = data.val & data.position;
                            data.position >>= 1;
                            if (data.position == 0) {
                                data.position = reset;
                                data.val = code(data.index++);
                            }
                            bits |= (resb > 0 ? 1 : 0) * power;
                            power <<= 1;
                        }
                        c = String.fromCharCode(bits);
                        break;
                    case 2:
                        return '';
                }
                dict[3] = c;
                w = c;
                result.push(c);
                while (true) {
                    if (data.index > length) {
                        return '';
                    }

                    bits = 0;
                    maxpower = Math.pow(2, numBits);
                    power = 1;
                    while (power != maxpower) {
                        resb = data.val & data.position;
                        data.position >>= 1;
                        if (data.position == 0) {
                            data.position = reset;
                            data.val = code(data.index++);
                        }
                        bits |= (resb > 0 ? 1 : 0) * power;
                        power <<= 1;
                    }

                    switch (c = bits) {
                        case 0:
                            bits = 0;
                            maxpower = Math.pow(2, 8);
                            power = 1;
                            while (power != maxpower) {
                                resb = data.val & data.position;
                                data.position >>= 1;
                                if (data.position == 0) {
                                    data.position = reset;
                                    data.val = code(data.index++);
                                }
                                bits |= (resb > 0 ? 1 : 0) * power;
                                power <<= 1;
                            }

                            dict[dictSize++] = String.fromCharCode(bits);
                            c = dictSize - 1;
                            enlargeIn--;
                            break;
                        case 1:
                            bits = 0;
                            maxpower = Math.pow(2, 16);
                            power = 1;
                            while (power != maxpower) {
                                resb = data.val & data.position;
                                data.position >>= 1;
                                if (data.position == 0) {
                                    data.position = reset;
                                    data.val = code(data.index++);
                                }
                                bits |= (resb > 0 ? 1 : 0) * power;
                                power <<= 1;
                            }
                            dict[dictSize++] = String.fromCharCode(bits);
                            c = dictSize - 1;
                            enlargeIn--;
                            break;
                        case 2:
                            return result.join('');
                    }

                    if (enlargeIn == 0) {
                        enlargeIn = Math.pow(2, numBits);
                        numBits++;
                    }

                    if (dict[c]) {
                        entry = dict[c];
                    } else {
                        if (c === dictSize) {
                            entry = w + w.charAt(0);
                        } else {
                            return '';
                        }
                    }
                    result.push(entry);

                    dict[dictSize++] = w + entry.charAt(0);
                    enlargeIn--;

                    w = entry;

                    if (enlargeIn == 0) {
                        enlargeIn = Math.pow(2, numBits);
                        numBits++;
                    }
                }
            },

            // 将指定的字符串转换为 base64 格式，并指示是否压缩字符串。
            toBase64: function (s, compress) {
                return !s ? '' : ($$.util.binary.toBase64(compress ? $$.util.string.compress(s) : $$.util.string.toUtf8(s)));
            },

            // 将 base64 转换为字符串，并指示是否解压缩字符串。
            fromBase64: function (s, decompress) {
                return !s ? '' : (decompress ? $$.util.string.decompress($$.util.binary.fromBase64(s)) : $$.util.string.fromUtf8($$.util.binary.fromBase64(s)));
            },

            // 比较两个字符串的大小，用于按字符串升序排序
            compareAsc: function (x, y) {
                if (!x && !y) return 0;
                if (!x) return -1;
                if (!y) return 1;
                return x.localeCompare(y);
            },

            // 比较两个字符串的大小，用于按字符串降序排序
            compareDesc: function (x, y) {
                return -$$.util.string.compareAsc(x, y);
            }
        };

        // 与 UUID 相关的函数
        $$.util.uuid = {
            // 创建一个新的 v4 版本的 UUID。fmt 取值如下：
            // D/d: 00000000-0000-0000-0000-000000000000
            // B/b: {00000000-0000-0000-0000-000000000000}
            // N/n: 00000000000000000000000000000000
            // I/i: 默认值，22 个字符的对 URL 友好的 base64（'+' 和 '/' 分别使用 '-' 和 '_' 替换）
            v4: function (fmt) {
                var d = new Date().getTime();
                if (window.performance && typeof window.performance.now === "function") {
                    d += performance.now();
                }

                var s = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                    var r = (d + Math.random() * 16) % 16 | 0;
                    d = Math.floor(d / 16);
                    return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
                });

                switch (fmt) {
                    case 'D':
                        return s.toUpperCase();
                    case 'd':
                        return s.toLowerCase();
                    case 'N':
                        return s.replaceAll('-', '').toUpperCase();
                    case 'n':
                        return s.replaceAll('-', '').toLowerCase();
                    case 'B':
                        return '{' + s.toUpperCase() + '}';
                    case 'b':
                        return '{' + s.toLowerCase() + '}';
                    default:
                        return $$.util.uuid.toBase64(s);
                }
            },

            // 将一个 UUID 转换为 base64 格式（'+' 和 '/' 分别使用 '-' 和 '_' 替换）
            toBase64: function (s) {
                if (!s) return '';
                if (typeof s === 'string') {
                    s = s.replaceAll('-', '');
                    if (s.charAt(0) == '{') {
                        s = s.substr(1, 32);
                    }

                    if (s.length == 32) {
                        var b = [];
                        for (var i = 0; i < s.length; i += 2) {
                            b.push(parseInt(s.substr(i, 2), 16));
                        }

                        return $$.util.binary.toBase64(b).replace(/\+/g, '-').replaceAll('/', '_').substr(0, 22);
                    }
                } else {
                    // 将参数看作字节数组
                    return $$.util.binary.toBase64(s).replace(/\+/g, '-').replaceAll('/', '_').substr(0, 22);
                }

                return '';
            },

            // 将一个 base64 字符串转化为 UUID。fmt 默认为 D。
            fromBase64: function (s, fmt) {
                if (!s || typeof s !== 'string' || (s.length != 22 && s.length != 24)) return '';
                var b = $$.util.binary.fromBase64(s.replaceAll('_', '/').replaceAll('-', '+') + (s.length == 22 ? '==' : ''));
                if (b.length == 16) {
                    s = '';
                    for (var i = 0; i < b.length; ++i) {
                        s = s + b[i].toString(16).padLeft('0', 2);
                    }

                    switch (fmt) {
                        case 'd':
                            return (s.substr(0, 8) + '-' + s.substr(8, 4) + '-' + s.substr(12, 4) + '-' + s.substr(16, 4) + '-' + s.substr(20)).toLowerCase();
                        case 'N':
                            return s.toUpperCase();
                        case 'n':
                            return s.toLowerCase();
                        case 'B':
                            return '{' + (s.substr(0, 8) + '-' + s.substr(8, 4) + '-' + s.substr(12, 4) + '-' + s.substr(16, 4) + '-' + s.substr(20)).toUpperCase() + '}';
                        case 'b':
                            return '{' + (s.substr(0, 8) + '-' + s.substr(8, 4) + '-' + s.substr(12, 4) + '-' + s.substr(16, 4) + '-' + s.substr(20)).toLowerCase() + '}';
                        default:
                            return (s.substr(0, 8) + '-' + s.substr(8, 4) + '-' + s.substr(12, 4) + '-' + s.substr(16, 4) + '-' + s.substr(20)).toUpperCase();
                    }
                }

                return '';
            }
        };

        // 与数组有关的函数
        $$.util.array = {
            // 对指定的数组进行排序
            sort: function (v, s) {
                if (v && v.length && Array.isArray(v) && typeof s === 'function') {
                    v.sort(s);
                }
                return v;
            }
        };

        const MIN_DATE = new Date(1970, 1, 1),
            MAX_DATE = new Date(9999, 12, 31, 23, 59, 59);

        function ensureDate(v, defaultValue) {
            if (v === null || typeof v === 'undefined') {
                return defaultValue;
            }

            if (v instanceof Date) {
                return v;
            }

            var date = Date.parse(v.toString());
            return isNaN(date) ? defaultValue : new Date(date);
        }

        // 类型或语义判断
        $$.util.is = {
            // 判断指定的字符串是否是有效的标识符
            identifier: function (s, max) {
                max = max || 30;
                return typeof s === 'string' && s.length > 0 && s.length <= max && /^[a-zA-Z][a-zA-Z0-9_]{0,127}$/i.test(s);
            },

            // 判断指定的字符串是否是有效的 UUID/GUID
            uuid: function (s) {
                return typeof s === 'string' &&
                    ((s.length == 22 && /^[a-zA-F0-9_\-]{22}$/i.test(s)) ||
                        (s.length == 24 && /^[a-zA-F0-9_\-]{22}==$/i.test(s)) ||
                        (s.length == 32 && /^[a-fA-F0-9]{32}$/i.test(s)) ||
                        (s.length == 36 && /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/i.test(s)));
            },

            // 判断指定的字符串是否是有效的电子邮件地址
            email: function (s, max) {
                max = max || 50;
                return typeof s === 'string' && s.length > 0 && s.length <= max && /^[0-9A-Za-z._%-]+@[0-9A-Za-z.-]+[.][0-9A-Za-z]{2,4}$/i.test(s);
            },

            // 判断指定的字符串是否是有效的手机号码
            mobile: function (s) {
                return typeof s === 'string' && s.length == 11 && /^1[3|4|5|7|8][0-9][0-9]{8}$/i.test(s);
            },

            // 判断指定的字符串是否是有效的密码
            password: function () {
                var s = arguments.length > 0 ? arguments[0] : '';

                if (!s) {
                    return false;
                }

                var cmp = arguments.length > 1 ? arguments[1] : '';
                var max = arguments.length > 2 ? arguments[2] : 30;

                if (typeof cmp === 'number') {
                    max = cmp;
                    cmp = null;
                }

                max = max || 30;
                if (typeof s === 'string' && s.length >= 6 && s.length <= max) {
                    if (cmp) {
                        return cmp === s;
                    } else {
                        return true;
                    }
                }

                return false;
            },

            // 判断指定的字符串是否是有效的数字和字母验证码
            verifyingCode: function (s) {
                return typeof s === 'string' && s.length == 6 && /^[a-zA-Z0-9]{6}$/i.test(s);
            },

            // 判断指定的值是不是一个日期时间
            datetime: function (v, min, max) {
                v = ensureDate(v, null);

                if (!v) {
                    return false;
                }

                min = ensureDate(min, MIN_DATE);
                max = ensureDate(max, MAX_DATE);

                return v >= min && v <= max;
            },

            // 判断指定的值是不是一个整数（不超过32位）
            int: function (v, min, max) {
                if (v === null || typeof v === 'undefined') {
                    return false;
                }

                if (typeof v !== 'number') {
                    v = $$.util.int.parse(v.toString());
                }

                min = Math.max(typeof min === 'number' && !Number.isNaN(min) ? min : -2147483648, -2147483648);
                max = Math.min(typeof max === 'number' && !Number.isNaN(max) ? max : 2147483647, 2147483647);

                return typeof v === 'number' && !Number.isNaN(v) && v >= min && v <= max;
            },

            // 判断指定的值是不是一个64位整数
            long: function (v, min, max) {
                if (v === null || typeof v === 'undefined') {
                    return false;
                }

                try {
                    v = Long.fromValue(v);
                } catch (e) {
                    return false;
                }

                try {
                    min = Long.fromValue(v);
                } catch (e) {
                    min = Long.MIN_VALUE;
                }

                try {
                    max = Long.fromValue(v);
                } catch (e) {
                    max = Long.MAX_VALUE;
                }

                return v.gte(min) && v.lte(max);
            },

            // 判断指定的值是不是一个数字
            float: function (v, min, max) {
                if (v === null || typeof v === 'undefined') {
                    return false;
                }

                if (typeof v !== 'number') {
                    v = $$.util.float.parse(v.toString());
                }

                min = typeof min === 'number' && !Number.isNaN(min) ? min : Number.NEGATIVE_INFINITY;
                max = typeof max === 'number' && !Number.isNaN(max) ? max : Number.POSITIVE_INFINITY;
                return typeof v === 'number' && !Number.isNaN(v) && v >= min && v <= max;
            },

            // 判断指定的值是不是一个布尔值
            bool: function (v) {
                if (v === null || typeof v === 'undefined') {
                    return false;
                }

                if (typeof v === 'boolean') {
                    return true;
                }

                v = v.toString().toLowerCase();
                return v == 'true' || v == 'false' || v == 't' || v == 'f' ||
                    v == '1' || v == 'yes' || v == 'y' || v == 'on' || v == '是' ||
                    v == '0' || v == 'no' || v == 'n' || v == 'off' || v == '否';
            },

            // 检查字符串是不是 'asc' 或 'desc'
            orderDirection: function (v) {
                if (!v) {
                    return false;
                }

                v = v.toString().toLowerCase();
                return v == 'asc' || v == 'desc';
            }
        };

        // 获取指定查询字符串字段的值
        $$.util.param = function (name, url) {
            url = url || window.location.href;
            name = name.replace(/[\[\]]/g, '\\$&');
            var rgx = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
                v = rgx.exec(url);
            if (!v) return null;
            if (!v[2]) return '';
            return decodeURIComponent(v[2].replace(/\+/g, " "));
        };
    })();

    // 提供与 UI 相关的功能
    (function () {
        $$.ui = {
            // 创建一个分页区
            pager: function (pageIndex, pageSize, totalRows, btnCount, $pager, goto) {
                if (!$pager || !$pager.length) {
                    $pager = $('<ul class="pagination"></ul>');
                } else {
                    $pager.empty();
                }

                --btnCount;
                btnCount = Math.max(0, btnCount);

                // 总页数
                var totalPages = Math.max(Math.ceil(totalRows / pageSize), 1);
                var min = pageIndex - Math.max(Math.floor(btnCount / 2), 0);
                var max = min + btnCount;

                if (min < 1) {
                    max = max + 1 - min;
                    min = 1;
                }

                if (max > totalPages) {
                    min = min - (max - totalPages);
                    max = totalPages;
                }

                min = Math.max(min, 1);
                max = Math.min(max, totalPages);

                // prev
                $pager.append('<li' + (pageIndex > 1 ? '' : ' class="disabled"') + '><a class="prev-page" href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a></li>');

                for (var i = min; i <= max; ++i) {
                    if (i == pageIndex) {
                        $pager.append('<li class="active"><a class="current-page" href="#">' + i + '</span></li>');
                    } else {
                        $pager.append('<li><a class="goto-page" href="#">' + i + '</a></li>');
                    }
                }

                // next
                $pager.append('<li' + (pageIndex < totalPages ? '' : ' class="disabled"') + '><a class="next-page" href="#" aria-label="Next"><span aria-hidden="true">&raquo;</span></a></li>');

                // 事件
                if ($.isFunction(goto)) {
                    $pager.find('.prev-page').on('click', function (e) {
                        e.preventDefault();
                        goto('prev');
                    });

                    $pager.find('.next-page').on('click', function (e) {
                        e.preventDefault();
                        goto('next');
                    });

                    $pager.find('.current-page').on('click', function (e) {
                        e.preventDefault();
                    });

                    $pager.find('.goto-page').on('click', function (e) {
                        e.preventDefault();
                        goto(parseInt($(this).text(), 10));
                    });
                }

                return $pager;
            },

            // 显示消息对话框
            alert: function (msg) {
                var title = arguments.length > 1 ? arguments[1] : '提示';
                var cb = arguments.length > 2 ? arguments[2] : null;

                if (title && typeof title === 'function') {
                    cb = title;
                    title = '提示';
                }

                var $m = $('#__pittypatAlertModal');
                if (!$m.length) {
                    $('body').append($$.ui.modalHtml('__pittypatAlert', '提示',
                        '<p class="text-warning auto-wrap"></p>',
                        '<button type="button" class="btn btn-default" data-dismiss="modal" autocomplete="off">关闭</button>'));
                    $m = $('#__pittypatAlertModal');
                }

                // 显示消息
                $m.find('.modal-body>p').multiline(msg);
                // 标题
                $('#__pittypatAlertModalLabel').text(title || '提示');

                if (cb && typeof cb == 'function') {
                    $m.one('hidden.bs.modal', cb).modal('show');
                } else {
                    $m.modal('show');
                }
            },

            set msg(s) {
                if (s === null || typeof s === 'undefined') {
                    s = '';
                } else {
                    s = s.toString();
                }

                var $msg = $('.pt-page-msg');
                if ($msg.length == 0) {
                    $msg = $('<div class="pt-page-msg"><div><i class="material-icons">warning</i><b></b></div></div>').appendTo('body');
                    $msg.on('click', function () {
                        $msg.css('display', 'none');
                        $body.removeClass('pt-overlapped');
                    });
                }

                if (s == '...' || s.toLowerCase() == 'loading') {
                    $msg.addClass('pt-page-msg-loading').find('>div>i').text('filter_tilt_shift').addClass('spin').next().text('');
                } else {
                    $msg.removeClass('pt-page-msg-loading').find('>div>i').removeClass('spin').text('warning').next().text(s);
                }

                $msg.css('display', s.length > 0 ? 'block' : 'none');
            },

            // 显示确认对话框
            confirm: function (title, msg, ok, cancel) {
                if (!msg) {
                    return;
                }

                var $dlg = $('.pt-confirm-dialog');

                if (!$dlg.length) {
                    var $parent = $('.pt-menus-dialogs');
                    if (!$parent.length) {
                        $parent = $('<div class="pt-menus-dialogs"></div>').appendTo('body');
                    }

                    $dlg = $(
                        '<div class="mdc-dialog pt-confirm-dialog small-dialog">' +
                        '<article class="mdc-dialog__surface">' +
                        '<header class="mdc-dialog__header">' +
                        '<h2 class="mdc-dialog__header__title"></h2>' +
                        '</header>' +
                        '<section class="mdc-dialog__body mdc-dialog__body--scrollable">' +
                        '<div></div>' +
                        '</section>' +
                        '<footer class="mdc-dialog__footer">' +
                        '<button type="button" class="mdc-button mdc-dialog__footer__button mdc-dialog__footer__button--cancel">取消</button>' +
                        '<button type="button" class="mdc-button mdc-dialog__footer__button mdc-button--primary mdc-dialog__footer__button--accept">确定</button>' +
                        '</footer>' +
                        '</article>' +
                        '<div class="mdc-dialog__backdrop"></div>' +
                        '</div>').appendTo($parent);
                }

                var dlg = $dlg.data('dlg');
                if (!dlg) {
                    dlg = new mdc.dialog.MDCDialog($dlg[0]);
                }

                function onAccept() {
                    dlg.unlisten('MDCDialog:accept', onAccept);
                    dlg.unlisten('MDCDialog:cancel', onCancel);

                    $$.val(ok);
                }

                function onCancel() {
                    dlg.unlisten('MDCDialog:accept', onAccept);
                    dlg.unlisten('MDCDialog:cancel', onCancel);

                    $$.val(cancel);
                }

                dlg.listen('MDCDialog:accept', onAccept);
                dlg.listen('MDCDialog:cancel', onCancel);

                $dlg.find('.mdc-dialog__header__title').text(title || '请确认');
                $dlg.find('.mdc-dialog__body>div').text(msg);

                dlg.show();
            }
        };
    })();

    // 提供远程服务器访问相关的功能
    (function () {
        function _default_validate() {
            return true;
        }

        function _default_before() {
            return false;
        }

        function _default_progress(per, msg) {
            console.log(per + ':' + msg);
        }

        function _default_success(val) {
            console.log(val);
        }

        function _default_load() {
        }

        function _default_error(s) {
            $$.ui.msg = s;
        }

        function _append_user_token(data) {
            var t = localStorage.getItem('__USER_TOKEN') || '';
            if (data) {
                if (typeof data === 'object') {
                    data['__USER_TOKEN'] = t;
                    return data;
                } else if (typeof data === 'string') {
                    data += '&__USER_TOKEN=' + encodeURIComponent(t);
                    return data;
                }
            }

            return { __USER_TOKEN: t };
        }

        function _is_valid(url, validate, error, args, _api) {
            var v = false;

            try {
                v = validate.call(_api, args);
            } catch (e) {
                // 执行验证时发生错误
                console.log(e);
                error.call(_api, e.message, args, null);
                return false;
            }

            if (typeof v === 'string') {
                if (v.length) {
                    // 返回了错误消息
                    error.call(_api, v, args, null);
                    return false;
                }
            } else {
                if (v === false) {
                    // 验证失败，但是没有提供错误消息
                    if (typeof url === 'string') {
                        error.call(_api, '无法请求路径 ' + url + '，验证程序返回了：' + v + '。', args, null);
                    } else {
                        error.call(_api, '无法提交请求，验证程序返回了：' + v + '。', args, null);
                    }
                    return false;
                }
            }

            return true;
        }

        function _skt_url(url) {
            if (!url || !url.length) {
                throw new Error('没有提供有效的请求路径。');
            }

            // 去掉 url 中 '?' 之后的部分
            var idx = url.indexOf('?');

            if (idx >= 0) {
                url = url.substr(0, idx);
            }

            if (!url.length) {
                throw new Error('没有提供有效的请求路径。');
            }

            // 请求路径的协议
            var p = window.location.protocol.toLowerCase();

            // 如果包含要发送的数据，在请求中使用 __data 进行指示，以便服务器能够接收和处理这些数据
            return (p == 'https:' ? 'wss:' : 'ws:') + "//" + window.location.host + url;
        }

        function _get_files_info(files) {
            if (!files || !files.length) {
                return '没有提供上传文件。';
            }

            if (files.length > 20) {
                return '一次上传的文件不能超过 20 个。';
            }

            var f = null;
            var info = {
                size: 0,
                count: files.length
            };

            for (var i = 0; i < files.length; ++i) {
                f = files[i];

                if (!f) {
                    return '上传文件列表中包含无效的文件：未提供的文件。';
                }

                if (!f.name) {
                    return '上传文件列表中包含无效的文件：未提供文件名称。';
                }

                if (!f.size) {
                    return '上传文件列表中包含无效的文件：' + f.name + ' 未提供文件大小或文件长度为 0。';
                }

                if (f.size > 2147483647) {
                    return '单个上传文件不能超过 2 GB：' + f.name + ' 的长度为 ' + (f.size / 1073741824).toFixed(2) + ' GB。';
                }

                info['n' + (i + 1)] = f.name;
                info['z' + (i + 1)] = f.size;
                info.size += f.size;
            }

            return info;
        }

        function _upload_progress_msg(index, offset, info, files) {
            if (index >= info.count) {
                return '所有文件上传完毕。';
            }

            var file = files[index];
            return '正在上传 ' + file.name + '：' + (offset / 1024).toFixed(2) + ' KB / ' + (file.size / 1024).toFixed(2) + ' KB。';
        }

        function _upload_result(files, urls) {
            var v = [];
            for (var i = 0; i < files.length; ++i) {
                v[i] = {
                    name: files[i].name,
                    size: files[i].size,
                    url: urls[i],
                    type: files[i].type || ''
                };
            }

            return v;
        }

        function _upload(url, files, progress, success, load, error, args, cancelButton, _api) {
            var index = -1, // 正在上传的文件在 files 中的位置
                sent = 0, // 已经上传的总字节数
                offset = 0, // 正在上传的文件偏移
                urls = [], // 已经上传的文件的虚拟路径
                sliceSize = 8192, // 上传文件的分片尺寸
                info = _get_files_info(files); // 上传文件的信息

            if (typeof info == 'string') {
                error.call(_api, info, args, files);
                return;
            }

            // 添加 __CSRF_TOKEN__ 参数
            info = _append_user_token(info);

            var skt = null;
            try {
                skt = new WebSocket(_skt_url(url) + '?' + $.param(info), 'upload.pittypat');
            } catch (e) {
                console.log(e);
                error.call(_api, e.message, args, files);
                return;
            }

            if (cancelButton) {
                $(cancelButton).one('click', function () {
                    skt.close();
                    error.call(_api, '用户取消了上传文件。', args, files);
                });
            }

            skt.onerror = function (e) {
                skt.close();
                console.log(e);
                error.call(_api, '网络通讯发生错误，请稍后再试。', args, files);
            };

            skt.onmessage = function (e) {
                if (!e.data) {
                    return;
                }

                var file = null,
                    uploadingSize = 0;

                try {
                    var msg = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;

                    if (!msg) {
                        skt.close();
                        error.call(_api, '没有接收到任何消息。', args, files);
                    } else if (msg.type == 'error') {
                        skt.close();
                        error.call(_api, msg.message, args, files);
                    } else if (msg.type == 'uploading') {
                        // 成功上传文件的一个 slice
                        uploadingSize = typeof msg.size === 'string' ? parseInt(msg.size, 10) : msg.size;
                        sent += uploadingSize;
                        offset += uploadingSize;

                        // 汇报上传进度
                        progress.call(_api, sent >= info.size ? 100 : (sent / info.size * 100), _upload_progress_msg(index, offset, info, files), args, files);

                        if (skt.readyState != 1) {
                            skt.close();
                            error.call(_api, '连接已经关闭。', args, files);
                        } else {
                            // 继续上传当前文件
                            file = files[index];
                            skt.send(file.slice(offset, offset + Math.min(file.size - offset, sliceSize)));
                        }
                    } else if (msg.type == 'uploaded') {
                        if (index < 0) {
                            // 上传第一个文件，第一个接收到 uploaded 消息中包含了要对文件进行分块的尺寸
                            sliceSize = typeof msg.size === 'string' ? parseInt(msg.size, 10) : msg.size;
                            sent = 0;
                            index = 0;
                            offset = 0;

                            // 汇报进度
                            progress.call(_api, 0, _upload_progress_msg(index, offset, info, files), args, files);

                            if (skt.readyState != 1) {
                                skt.close();
                                error.call(_api, '连接已经关闭。', args, files);
                            } else {
                                file = files[index];
                                skt.send(file.slice(0, Math.min(file.size, sliceSize)));
                            }
                        } else {
                            // 一个文件上传成功，记录刚刚上传文件的虚拟路径
                            urls.push(msg.url);

                            // 汇报进度
                            sent += typeof msg.size === 'string' ? parseInt(msg.size, 10) : msg.size;
                            ++index;

                            progress.call(_api, sent >= info.size ? 100 : (sent / info.size * 100), _upload_progress_msg(index, offset, info, files), args, files);

                            // 是否所有文件都已经上传完毕
                            if (index < info.count) {
                                // 继续上传下一个文件
                                offset = 0;

                                if (skt.readyState != 1) {
                                    skt.close();
                                    error.call(_api, '连接已经关闭。', args, files);
                                } else {
                                    file = files[index];
                                    skt.send(file.slice(0, Math.min(file.size, sliceSize)));
                                }
                            } else {
                                // 所有文件上传完毕
                                skt.close();
                                success.call(_api, _upload_result(files, urls), args, files);
                                load.call(_api, args, files);
                            }
                        }
                    } else {
                        skt.close();
                        console.log(msg);
                        error.call(_api, '文件上传过程中接收到未预期的消息。', args, files);
                    }
                } catch (err) {
                    skt.close();
                    console.log(err);
                    error.call(_api, (e.message || '') + '\r\n' + e.data, args, files);
                }
            };

            return skt;
        }

        function _upload_ajax(o, _api) {
            var ajaxSuccess = o.success,
                ajaxLoad = o.load;
            o.load = _default_load;
            o.success = function (files, args) {
                $$.web.ajax({
                    url: o.url,
                    args: args,
                    data: function () {
                        if (typeof o.data === 'function') {
                            return o.data.call(_api, files, args);
                        } else {
                            return o.data;
                        }
                    },
                    success: ajaxSuccess,
                    error: o.error,
                    load: ajaxLoad
                }, _api);
            }

            return $$.web.upload(o);
        }

        function _upload_ws(o, _api) {
            var wsLoad = o.load;
            o.progress = o.uploadProgress;
            o.load = _default_load;
            o.success = function (files, args) {
                $$.web.ws({
                    url: o.url,
                    args: args,
                    data: function () {
                        if (typeof o.data === 'function') {
                            return o.data.call(_api, files, args);
                        } else {
                            return o.data;
                        }
                    },
                    progress: o.wsProgress,
                    error: o.error,
                    load: wsLoad
                }, _api);
            }

            return $$.web.upload(o, _api);
        }

        $$.web = {
            // 初始化应用的根路径，仅在首页或位于 App 根路径中的页面中调用此函数
            initAppPath: function () {
                try {
                    var s = window.location.pathname;
                    s = s.substr(0, s.lastIndexOf('/'));

                    localStorage.removeItem('__APP_PATH');
                    localStorage.setItem('__APP_PATH', s);

                    return s;
                } catch (e) {
                    console.log(e);
                    return '';
                }
            },

            // 获取站点的根路径
            get appPath() {
                try {
                    return localStorage.getItem('__APP_PATH') || '';
                } catch (e) {
                    console.log(e);
                    return '';
                }
            },

            // 获取指定路径（使用 '~/' 开头的路径）的完整的绝对路径
            url: function (url) {
                if (!url || typeof url != 'string') {
                    return '';
                }

                if (url.charAt(0) != '~') {
                    return url;
                }

                if (url.length > 0) {
                    if (url.charAt(1) == '/' || url.charAt(1) == '\\') {
                        url = url.substr(2); // 去掉 '~/'
                    } else {
                        url = url.substr(1); // 去掉 '~'
                    }
                } else {
                    url = '';
                }

                return $$.web.appPath + '/' + url;
            },

            // 执行 ajax 请求
            ajax: function (opt, _api) {
                var o = $$.val(opt);

                if (!o || typeof o !== 'object') {
                    throw new Error('没有提供有效的 ajax 请求参数。');
                }

                var error = !o.error || typeof o.error !== 'function' ? _default_error : o.error,
                    args = null,
                    url = null,
                    data = null;

                try {
                    // 获取参数
                    args = $$.val(o.args, null, _api);
                    // 请求路径
                    url = $$.web.url($$.val(o.url, args, _api));
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, data);
                    return;
                }

                // 参数验证方法
                var validate = !o.validate || typeof o.validate !== 'function' ? _default_validate : o.validate;

                // 验证参数，并在验证失败后执行 error
                if (!_is_valid(url, validate, error, args, _api)) {
                    return;
                }

                // 获取请求路径和请求数据
                try {
                    // 请求数据
                    data = $$.val(o.data, args, _api);
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, data);
                    return;
                }

                // 添加 __CSRF_TOKEN__ 参数
                data = _append_user_token(data);

                var before = !o.before || typeof o.before !== 'function' ? _default_before : o.before,
                    load = !o.load || typeof o.load !== 'function' ? _default_load : o.load;

                if (before.call(_api, args, data)) {
                    // 取消请求
                    load.call(_api, args, data);
                    return;
                }

                var success = !o.success || typeof o.success !== 'function' ? _default_success : o.success;

                return $.ajax({
                    async: true,
                    type: 'POST',
                    contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
                    url: url,
                    data: data,
                    dataType: 'json',
                    success: function (msg) {
                        try {
                            if (!msg || msg.type == 'empty') {
                                success.call(_api, null, args, null, data);
                                load.call(_api, args, data);
                            } else if (msg.type == 'error') {
                                error.call(_api, msg.message, args, data);
                            } else if (msg.type == 'value') {
                                success.call(_api, msg.value, args, msg.valueType, data);
                                load.call(_api, args, data);
                            } else if (msg.type == 'user') {
                                success.call(_api, msg.info, args, null, data);
                                load.call(_api, args, data);
                            } else {
                                success.call(_api, msg, args, null, data);
                                load.call(_api, args, data);
                            }
                        } catch (err) {
                            console.log(err);
                            error.call(_api, err.message, args, data);
                        }
                    },
                    error: function (x, s, e) {
                        console.log(x);
                        console.log(e);

                        if (s == 'parsererror') {
                            error.call(_api, 'AJAX 解析 ' + url + ' 如下 JSON 字符串时发生错误：\r\n' + x.responseText + '\r\n\r\n错误信息如下：\r\n' + (typeof e == 'string' ? e : (e.message || '')), args, data);
                        } else if (s == 'error') {
                            error.call(_api, 'AJAX 访问 ' + url + ' 时发生错误：\r\n' + (typeof e == 'string' ? e : (e.message || '')), args, data);
                        } else if (s == 'timeout') {
                            error.call(_api, 'AJAX 访问 ' + url + ' 超时。', args, data);
                        } else if (s == 'abort') {
                            error.call(_api, 'AJAX 访问 ' + url + ' 被取消。', args, data);
                        } else {
                            error.call(_api, '无法访问 ' + url + '：' + (x.status == 200 ? x.responseText : (x.status + ' - ' + x.statusText)), args, data);
                        }
                    }
                });
            },

            // 使用 WebSocket 执行请求
            ws: function (opt, _api) {
                var o = $$.val(opt);

                if (!o || typeof o !== 'object') {
                    throw new Error('没有提供有效的 WebSocket 请求参数。');
                }

                var error = !o.error || typeof o.error !== 'function' ? _default_error : o.error,
                    args = null,
                    url = null,
                    data = null;

                try {
                    args = $$.val(o.args, null, _api);
                    url = $$.web.url($$.val(o.url, args, _api));
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, data);
                    return;
                }

                var validate = !o.validate || typeof o.validate !== 'function' ? _default_validate : o.validate;

                if (!_is_valid(url, validate, error, args, _api)) {
                    return;
                }

                try {
                    data = $$.val(o.data, args, _api);
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, data);
                    return;
                }

                // 添加 __CSRF_TOKEN__ 参数，.NET 中从发送的数据中解析令牌
                data = _append_user_token(data);

                var before = !o.before || typeof o.before !== 'function' ? _default_before : o.before,
                    load = !o.load || typeof o.load !== 'function' ? _default_load : o.load;

                if (before.call(_api, args, data)) {
                    // 取消请求
                    load.call(_api, args, data);
                    return;
                }

                var skt = null;

                // 路径上附带的参数
                var params = {};
                if (data) {
                    // 指示服务器应该通过 socket 接收数据
                    params.__data = '1';
                }

                // 添加 __CSRF_TOKEN__ 参数，Java 中需要从查询字符串中解析用户令牌
                params = _append_user_token(params);

                try {
                    skt = new WebSocket(_skt_url(url) + '?' + $.param(params));
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, data);
                    return;
                }

                if (o.cancelButton) {
                    $(o.cancelButton).one('click', function () {
                        skt.close();
                        error.call(_api, '操作已取消。', args, data);
                    });
                }

                skt.onerror = function (e) {
                    if (skt) {
                        skt.close();
                        skt = null;
                    }
                    console.log(e);
                    error.call(_api, '网络通讯发生错误，请稍后再试。', args, data);
                };

                var progress = !o.progress || typeof o.progress !== 'function' ? _default_progress : o.progress,
                    success = !o.success || typeof o.success !== 'function' ? _default_success : o.success;

                skt.onmessage = function (e) {
                    if (!e.data) {
                        return;
                    }

                    try {
                        var msg = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;

                        if (!msg) {
                            skt.close();
                            error.call(_api, '没有接收到任何消息。', args, data);
                        } else if (msg.type == 'error') {
                            skt.close();
                            error.call(_api, msg.message, args, data);
                        } else if (msg.type == 'progress') {
                            if (msg.percent >= 100) {
                                skt.close();
                                progress.call(_api, msg.percent, msg.message, msg.value, args, msg.valueType, data);
                                success.call(_api, msg.value, args, msg.valueType, data);
                                load.call(_api, args, data);
                            } else {
                                progress.call(_api, msg.percent, msg.message, msg.value, args, msg.valueType, data);
                            }
                        } else if (msg.type == 'value') {
                            skt.close();
                            progress.call(_api, 100, '', msg.value, args, msg.valueType, data);
                            load.call(_api, args, data);
                        } else {
                            console.log(msg);
                            error.call(_api, '接收到未预期的消息：' + e.data, args, data);
                        }
                    } catch (err) {
                        skt.close();
                        console.log(err);
                        error.call(_api, e.message + '\r\n' + e.data, args, data);
                    }
                };

                // 发送 data
                if (data) {
                    // 向服务器发送数据
                    skt.onopen = function () {
                        try {
                            skt.send(typeof data === 'object' ? $.param(data) : ('' + data));
                        } catch (e) {
                            skt.close();
                            console.log(e);
                            error.call(_api, e.message, args, data);
                        }
                    };
                }

                return skt;
            },

            // 上传文件
            upload: function (opt, _api) {
                var o = $$.val(opt);

                if (!o || typeof o !== 'object') {
                    throw new Error('没有提供有效的文件上传请求参数。');
                }

                var error = !o.error || typeof o.error !== 'function' ? _default_error : o.error,
                    args = null,
                    url = null,
                    files = null;

                try {
                    args = $$.val(o.args, null, _api);
                    url = $$.web.url($$.val(o.url, args, _api));
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, files);
                    return;
                }

                var validate = !o.validate || typeof o.validate !== 'function' ? _default_validate : o.validate;

                if (!_is_valid(url, validate, error, args, _api)) {
                    return;
                }

                try {
                    files = $$.val(o.files, args, _api);
                } catch (e) {
                    console.log(e);
                    error.call(_api, e.message, args, files);
                    return;
                }

                var before = !o.before || typeof o.before !== 'function' ? _default_before : o.before,
                    load = !o.load || typeof o.load !== 'function' ? _default_load : o.load;

                if (before.call(_api, args, files)) {
                    // 取消请求
                    load.call(_api, args, files);
                    return;
                }

                var progress = !o.progress || typeof o.progress !== 'function' ? _default_progress : o.progress,
                    success = !o.success || typeof o.success !== 'function' ? _default_success : o.success;

                return _upload(url, files, progress, success, load, error, args, o.cancelButton, _api);
            }
        };

        // API 使用的事件对象
        function ApiEvent(target, type, props) {
            var me = this;
            switch (type) {
                case 'args':
                    me.args = null;
                    break;
                case 'method':
                    me.args = props.args;
                    me.method = 'POST';
                    break;
                case 'data':
                    me.args = props.args;
                    if (props.files) {
                        me.files = props.files;
                    }
                    me.data = null;
                    break;
                case 'files':
                    me.args = props.args;
                    me.files = null;
                    break;
                case 'validate':
                    me.args = props.args;
                    (function () {
                        var _valid = true, _message = '';
                        Object.defineProperty(me, 'valid', {
                            get: function () {
                                return _valid;
                            },
                            set: function (valid) {
                                _valid = valid;
                                if (valid) {
                                    _message = '';
                                }
                            }
                        });

                        Object.defineProperty(me, 'message', {
                            get: function () {
                                return _message;
                            },
                            set: function (message) {
                                _message = typeof message === 'string' ? message : message.toString();
                                _valid = _message.length < 1;
                            }
                        });
                    })();
                    break;
                case 'before':
                    me.args = props.args;
                    me.data = props.data;
                    me.cancel = false;
                    break;
                case 'success':
                    me.args = props.args;
                    me.data = props.data;
                    me.result = props.result;
                    me.resultType = props.resultType;
                    break;
                case 'progress':
                    me.args = props.args;
                    me.data = props.data;
                    me.period = props.period;
                    me.percent = props.percent;
                    me.message = props.message;
                    me.result = props.result;
                    me.resultType = props.resultType;
                    break;
                case 'error':
                    me.args = props.args;
                    me.data = props.data;
                    me.message = props.message;
                    break;
                default:
                    me.args = props.args;
                    me.data = props.data;
                    break;
            }

            Object.defineProperty(me, 'target', {
                get: function () {
                    return target;
                }
            });

            Object.defineProperty(me, 'type', {
                get: function () {
                    return type;
                }
            });
        }

        function EventHandlers() {
            var _items = [];

            function _findIndex(type) {
                for (var i = 0; i < _items.length; ++i) {
                    if (_items[i].type == type) {
                        return i;
                    }
                }
                return -1;
            }

            function _find(type) {
                for (var i = 0; i < _items.length; ++i) {
                    if (_items[i].type == type) {
                        return _items[i];
                    }
                }
                return null;
            }

            function _ensure(type) {
                var item = _find(type);

                if (!item) {
                    item = {
                        type: type,
                        handlers: []
                    };

                    _items.push(item);
                }

                return item;
            }

            function _handlerIndexOf(item, handler) {
                if (item && item.handlers.length && handler) {
                    for (var i = 0; i < item.handlers.length; ++i) {
                        if (item.handlers[i] === handler) {
                            return i;
                        }
                    }
                }

                return -1;
            }

            this.on = function (type, handler) {
                type = type.toLowerCase();
                _ensure(type).handlers.push(handler);
                return this;
            };

            this.off = function (type, handler) {
                type = type.toLowerCase();

                if (typeof handler === 'function') {
                    var item = _find(type);
                    if (item) {
                        var index = _handlerIndexOf(item, handler);
                        if (index >= 0) {
                            item.handlers.splice(index, 1);
                        }
                    }
                } else {
                    var ii = _findIndex(type);
                    if (ii >= 0) {
                        _items.splice(ii, 1);
                    }
                }

                return this;
            };

            this.trigger = function (api, type, props) {
                var item = _find(type),
                    evt = new ApiEvent(api, type, props),
                    i = 0;
                if (item) {
                    if (type == 'error') {
                        if (item.handlers.length) {
                            for (; i < item.handlers.length; ++i) {
                                item.handlers[i].call(api, evt);
                            }
                        } else {
                            // 没有提供错误处理事件，默认使用 msg 显示错误消息
                            if (evt.message) {
                                $$.ui.msg = evt.message;
                            }
                        }
                    } else {
                        if (item.handlers.length) {
                            for (; i < item.handlers.length; ++i) {
                                item.handlers[i].call(api, evt);
                            }
                        }
                    }
                } else {
                    if (type == 'error' && evt.message) {
                        // 没有提供错误处理事件，默认使用 msg 显示错误消息
                        $$.ui.msg = evt.message;
                    }
                }

                return evt;
            };
        }

        // 初始化一个 api 对象
        function initApi(api, opt) {
            opt = $$.val(opt);
            if (!opt) {
                opt = {};
            } else if (typeof opt === 'string') {
                opt = {
                    url: url
                };
            }

            var _url = null;
            if (typeof opt.url === 'function') {
                _url = opt.url;
            } else {
                _url = opt.url ? opt.url.toString() : null;
                if (!_url) {
                    throw new Error('没有提供 API 的请求路径。');
                }
            }

            api.url = _url;

            // 事件处理程序列表
            var _eventHandlers = new EventHandlers();

            // 注册一个事件处理程序
            function _on(type, handler) {
                if (!type && typeof type !== 'string') {
                    throw new Error('没有提供事件类型。');
                }

                if (typeof handler !== 'function') {
                    throw new Error('没有提供事件处理程序函数。');
                }

                _eventHandlers.on(type, handler);

                return api;
            }

            // 注销一个事件处理程序
            function _off(type, handler) {
                if (!type && typeof type !== 'string') {
                    throw new Error('没有提供事件类型。');
                }

                _eventHandlers.off(type, handler);

                return api;
            }

            // 注册一个一次性的事件处理程序
            function _one(type, handler) {
                if (!type && typeof type !== 'string') {
                    throw new Error('没有提供事件类型。');
                }

                if (typeof handler !== 'function') {
                    throw new Error('没有提供事件处理程序函数。');
                }

                var listener = function (e) {
                    _off(type, listener);
                    handler(e);
                };

                return _on(type, listener);
            }

            // 触发指定类型的事件处理程序
            function _trigger(type, props) {
                if (!type && typeof type !== 'string') {
                    throw new Error('没有提供事件类型。');
                }

                return _eventHandlers.trigger(api, type, props);
            }

            Object.defineProperty(api, 'on', {
                get: function () {
                    return _on;
                }
            });

            Object.defineProperty(api, 'one', {
                get: function () {
                    return _one;
                }
            });

            Object.defineProperty(api, 'off', {
                get: function () {
                    return _off;
                }
            });

            Object.defineProperty(api, 'trigger', {
                get: function () {
                    return _trigger;
                }
            });

            // 注册预定义在 opt 中 method
            if (typeof opt.method === 'function') {
                api.on('method', opt.method);
            } else if (opt.method) {
                var _method = opt.method.toString().toUpperCase();
                api.on('method', function (e) {
                    e.method = _method;
                });
            }

            // 注册预定义在 opt 中 args
            if (typeof opt.args === 'function') {
                api.on('args', opt.args);
            }

            // 注册预定义在 opt 中 data
            if (typeof opt.data === 'function') {
                api.on('data', opt.data);
            }

            // 注册预定义在 opt 中 files
            if (typeof opt.files === 'function') {
                api.on('files', opt.files);
            }

            // 注册预定义在 opt 中 validate
            if (typeof opt.validate === 'function') {
                api.on('validate', opt.validate);
            }

            // 注册预定义在 opt 中 before
            if (typeof opt.before === 'function') {
                api.on('before', opt.before);
            }

            // 注册预定义在 opt 中 success
            if (typeof opt.success === 'function') {
                api.on('success', opt.success);
            }

            // 注册预定义在 opt 中 progress
            if (typeof opt.progress === 'function') {
                api.on('progress', opt.progress);
            }

            // 注册预定义在 opt 中 load
            if (typeof opt.load === 'function') {
                api.on('load', opt.load);
            }

            // 注册预定义在 opt 中 error
            if (typeof opt.error === 'function') {
                api.on('error', opt.error);
            }

            return api;
        }

        $$.api = function (base) {
            var api = function () {
                var o = {};

                try {
                    // 参数
                    o.args = api.trigger('args').args;
                } catch (oe) {
                    console.log(oe);
                    api.trigger('error', {
                        args: null,
                        message: oe.message
                    });
                    return;
                }

                try {
                    // url
                    o.url = $$.val(api.url, o.args);
                    if (!o.url || typeof o.url !== 'string') {
                        api.trigger('error', {
                            args: o.args,
                            message: '没有提供有效的 API 请求路径。'
                        });
                        return;
                    }

                    // method
                    o.method = (api.trigger('method', o).method || '').toString().toUpperCase();

                    // validate
                    o.validate = function (args) {
                        var e = api.trigger('validate', {
                            args: args
                        });

                        if (e.valid) {
                            return true;
                        }

                        e.message = (e.message || '').toString();
                        return e.message.length > 0 ? e.message : false;
                    };

                    // before
                    o.before = function (args, data) {
                        return api.trigger('before', {
                            args: args,
                            data: data
                        }).cancel;
                    };

                    // success
                    o.success = function (result, args, resultType, data) {
                        api.trigger('success', {
                            result: result,
                            resultType: resultType,
                            args: args,
                            data: data
                        });
                    };

                    // load
                    o.load = function (args, data) {
                        api.trigger('load', {
                            args: args,
                            data: data
                        });
                    };

                    // error
                    o.error = function (s, args, data) {
                        api.trigger('error', {
                            message: s,
                            args: args,
                            data: data
                        });
                    };

                    if (o.method === 'WEBSOCKET' || o.method === 'WS') {
                        // WebSocket

                        // data
                        o.data = function () {
                            return api.trigger('data', o).data;
                        };

                        // progress
                        o.progress = function (percent, message, value, args, valueType, data) {
                            api.trigger('progress', {
                                period: 'ws',
                                percent: percent,
                                message: message,
                                result: value,
                                resultType: valueType,
                                args: args,
                                data: data
                            });
                        };

                        return $$.web.ws(o, api);
                    } else if (o.method === 'UPLOAD') {
                        // 只是上传

                        // files
                        o.files = function (args) {
                            return api.trigger('files', {
                                args: args
                            }).files;
                        };

                        // progress
                        o.progress = function (percent, message, args, data) {
                            api.trigger('progress', {
                                period: 'upload',
                                percent: percent,
                                message: message,
                                result: null,
                                resultType: null,
                                args: args,
                                data: data
                            });
                        };

                        return $$.web.upload(o, api);
                    } else if (o.method === 'UPLOAD-POST' || o.method === 'UPOST') {
                        // 上传后 POST

                        // files
                        o.files = function (args) {
                            return api.trigger('files', {
                                args: args
                            }).files;
                        };

                        // data
                        o.data = function (files, args) {
                            return api.trigger('data', {
                                files: files,
                                args: args
                            }).data;
                        };

                        // progress
                        o.progress = function (percent, message, args, data) {
                            api.trigger('progress', {
                                period: 'upload',
                                percent: percent,
                                message: message,
                                result: null,
                                resultType: null,
                                args: args,
                                data: data
                            });
                        };

                        return _upload_ajax(o, api);
                    } else if (o.method === 'UPLOAD-WS' || o.method === 'UWS') {
                        // 上传后 WebSocket

                        // files
                        o.files = function (args) {
                            return api.trigger('files', {
                                args: args
                            }).files;
                        };

                        // data
                        o.data = function (files, args) {
                            return api.trigger('data', {
                                files: files,
                                args: args
                            }).data;
                        };

                        // uploadProgress
                        o.uploadProgress = function (percent, message, args, data) {
                            api.trigger('progress', {
                                period: 'upload',
                                percent: percent,
                                message: message,
                                result: null,
                                resultType: null,
                                args: args,
                                data: data
                            });
                        };

                        // wsProgress
                        o.wsProgress = function (percent, message, value, args, valueType, data) {
                            api.trigger('progress', {
                                period: 'ws',
                                percent: percent,
                                message: message,
                                result: value,
                                resultType: valueType,
                                args: args,
                                data: data
                            });
                        };

                        return _upload_ws(o, api);
                    } else {
                        // POST

                        // data
                        o.data = function () {
                            return api.trigger('data', o).data;
                        };

                        return $$.web.ajax(o, api);
                    }
                } catch (oe) {
                    console.log(oe);
                    api.trigger('error', {
                        args: o.args,
                        message: oe.message
                    });
                }
            };

            return initApi(api, base);
        };
    })();

    // 应用命名空间
    (function () {
        // 开发者和应用的根命名空间
        $$.com = {};

        // 定义指定模块的命名空间，确保该命名空间是存在的
        $$.namespace = function (path) {
            var names = path.split('.');
            if (names.length != 3) {
                throw new Error(path + ' 不是一个有效的模块路径。');
            }

            if (!$$.util.is.identifier(names[0])) {
                throw new Error(names[0] + ' 不是一个有效的开发者代码。');
            }

            if (!$$.util.is.identifier(names[1])) {
                throw new Error(names[0] + ' 不是一个有效的应用标识符。');
            }

            if (!$$.util.is.identifier(names[2])) {
                throw new Error(names[0] + ' 不是一个有效的模块标识符。');
            }

            var dev = $$.com[names[0]], app = null, mod = null;
            if (!dev) {
                // 不存在开发者命名空间
                dev = $$.com[names[0]] = {};
                dev._code = names[0];
            }

            app = dev[names[1]];
            if (!app) {
                // 不存在应用命名空间
                app = dev[names[1]] = {};
                app._path = names[0] + '.' + names[1];
                app._developer = dev;
            }

            mod = app[names[2]];
            if (!mod) {
                mod = app[names[2]] = {};
                mod._path = path;
                mod._app = app;
                mod._define = function (iden, props) {
                    return define(mod, iden, props);
                };
            }

            return mod;
        };
    })();

    // 提供与当前用户相关的功能
    (function () {
        function _cookie_enabled() {
            var yes = navigator.cookieEnabled === true;

            if (typeof navigator.cookieEnabled === 'undefined' && !yes) {
                document.cookie = '_cookie_test';
                yes = document.cookie.indexOf('_cookie_test') >= 0;
            }

            return yes;
        }

        // 当前用户信息
        var __current = null;

        $$.user = {
            // 获取当前用户的用户信息
            get current() {
                return __current;
            },

            // 从服务器加载当前用户信息
            load: function (refresh) {
                if (!__current) {
                    refresh = true;
                }

                if (refresh) {
                    $$.web.ajax({
                        url: '~/user/info',
                        success: function (user) {
                            __current = user && $$.util.is.uuid(user.id) ? user : null;
                            invokeUserHandlers();
                        },
                        error: function (s) {
                            console.log(s);
                            $$.ui.msg = s;
                        }
                    });
                } else {
                    invokeUserHandlers();
                }
            },

            // 执行登录操作
            login: function (name, password, ok, error) {
                $$.web.ajax({
                    url: '~/user/login',
                    data: {
                        name: name,
                        password: password,
                        __cookie_enabled: _cookie_enabled()
                    },
                    success: function (res) {
                        var t = res && res.token && res.user && $$.util.is.uuid(res.user.id) ? res.token : null;
                        if (t) {
                            __current = res.user;
                            localStorage.setItem('__USER_TOKEN', t);
                            if (!$$.val(ok, __current)) {
                                invokeUserHandlers();
                            }
                        } else {
                            __current = null;
                            localStorage.removeItem('__USER_TOKEN');
                            $$.val(error, '登录失败，用户名或密码不正确。');
                            invokeUserHandlers();
                        }
                    },
                    error: function (s) {
                        if (typeof error === 'function') {
                            $$.val(error, s);
                        } else {
                            console.log(s);
                            $$.ui.msg = s;
                        }
                    }
                });
            },

            // 注销当前用户
            logout: function (done) {
                var final = function () {
                    localStorage.removeItem('__USER_TOKEN');
                    __current = null;

                    if (typeof done === 'function') {
                        done();
                    } else {
                        $$.user.goHome();
                    }

                    invokeUserHandlers();
                }

                $$.web.ajax({
                    url: '~/user/logout',
                    success: final,
                    error: final
                });
            },

            // 从当前页面跳转到首页
            goHome: function () {
                window.location.replace($$.web.appPath + '/');
            }
        };
    })();

    // 根据用户信息初始化导航列表、通知中心以及菜单
    (function () {
        // 导航列表在 localStorage 中保存的名称
        var NAV_LIST_KEY = '__NAV_LIST',
            // 导航列表隶属的用户的 Id 在 localStorage 中保存的名称
            NAV_USER_ID_KEY = '__NAV_USER_ID',
            // 导航面板
            $nav = $('body>nav'),
            // 通知中心面板
            $nc = $('body>aside');

        // 为登录用户加载导航列表
        function navOnAuthorized(_) {
            var
                // 导航列表
                $list = $('.pt-nav-panel>section'),
                // 刷新按钮
                $btnRefresh = $('.pt-nav-refresh'),
                // 单个模块条目的高度
                ITEM_MAX_HEIGHT = 56,
                // 当前页面的路径部分
                currentPath = window.location.pathname.toLowerCase();

            // 判断指定的路径是不是当前页面
            function isCurrent(url) {
                url = url.toLowerCase();
                return url == currentPath ||
                    (url + 'index.html') == currentPath ||
                    (url + 'index.html') == (currentPath + 'index.html') ||
                    url == (currentPath + 'index.html');
            }

            // 创建一个模块导航项目
            function createModuleItem($modList, mod) {
                // 模块对应静态页面的路径，该路径从服务器返回
                var url = $$.web.url(mod.url),
                    current = isCurrent(url);
                var $item = $(
                    '<a class="pt-nav-item' + (current ? ' page-nav-current' : '') + '" href="' + url + '">' +
                    '<b></b>' +
                    '<i class="material-icons">' + (current ? 'update' : 'arrow_forward') + '</i>' +
                    '</a>');

                // 名称
                $item.children('b').text(mod.name);

                // 工具提示
                if (mod.description) {
                    $item.attr('title', mod.description);
                }

                $modList.append($item);

                return current;
            }

            // 创建一个 App 导航项目
            function createAppItem(app) {
                // 如果目标 App 中没有任何模块，则不显示该 App
                if (app.modules && app.modules.length) {
                    var $group = $(
                        '<article class="pt-nav-group">' +
                        '<header class="mdc-ripple-surface" data-mdc-auto-init="MDCRipple">' +
                        '<span></span>' +
                        '<b></b>' +
                        '<i class="material-icons">expand_' + (app._collapsed ? 'more' : 'less') + '</i>' +
                        '</header>' +
                        '<section></section>' +
                        '</article>');

                    // 图标
                    if (app.iconUrl) {
                        $group.find('span').html('<img src="' + $$.web.url(app.iconUrl) + '" alt="" />');
                    } else {
                        $group.find('span').html('<i class="material-icons">cloud_circle</i>');
                    }

                    // 名称
                    $group.find('b').text(app.name);

                    // 工具提示
                    if (app.description) {
                        $group.children('header').attr('title', app.description);
                    }

                    // 模块项目列表
                    var $modList = $group
                        .toggleClass('pt-nav-group-expanded', !app._collapsed)
                        .children('section').data('hidden', app._collapsed).css('max-height', '0px');

                    // 添加模块列表
                    for (var i = 0; i < app.modules.length; ++i) {
                        if (createModuleItem($modList, app.modules[i])) {
                            $group.addClass('page-nav-current');
                        }
                    }

                    // 初始化模块列表的 max-height
                    if (!app._collapsed) {
                        $modList.css('max-height', ($modList.children().length * ITEM_MAX_HEIGHT) + 'px');
                    }

                    $list.append($group.attr('data-id', app.id));
                }
            }

            // 创建导航列表
            function createNavList(apps) {
                // 清空当前所有导航项目
                $list.empty();

                // 过滤掉没有模块的应用
                apps = apps.filter(function (a) {
                    return a.modules && a.modules.length > 0;
                });

                // 创建应用导航分组
                apps.each(function (a, i) {
                    createAppItem(a);

                    if (i < apps.length - 1) {
                        // 添加分隔线
                        $list.append('<hr />');
                    }
                });

                // 将当前导航项目滚动到当前窗口可见区域
                var $m = $('.pt-nav-item.page-nav-current');
                if ($m.length > 0) {
                    $m[0].scrollIntoView(false);
                }

                // 检查应用图标图片是否加载成功，如果不成功，则显示默认的图标
                $list.find('img').one('error', function () {
                    this.parentElement.innerHTML = '<i class="material-icons">crop_original</i>';
                });

                // 初始化 mdc
                $list.mdcInit();
            }

            // 从服务器加载并刷新导航列表
            function fromServer() {
                // 禁用刷新按钮
                $btnRefresh.prop('disabled', true);

                // 从服务器加载导航列表
                $$.web.ajax({
                    url: '~/user/navlist',
                    success: function (apps) {
                        apps = apps || [];

                        // 默认认为导航是展开的
                        for (var i = 0; i < apps.length; ++i) {
                            apps[i]._collapsed = false;
                        }

                        // 创建导航列表
                        createNavList(apps);

                        // 将导航项目保存到 localStorage
                        localStorage.setItem(NAV_LIST_KEY, $$.util.string.toBase64(JSON.stringify(apps)));
                        localStorage.setItem(NAV_USER_ID_KEY, _.user.id);

                        // 启用刷新按钮
                        $btnRefresh.prop('disabled', false);
                    },
                    error: function (s) {
                        // 显示错误消息
                        $$.ui.msg = s;
                        // 启用刷新按钮
                        $btnRefresh.prop('disabled', false);
                    }
                });
            }

            function loadNav(refresh) {
                var apps = null;

                if (!refresh) {
                    // 已经保存在 localStorage 中的导航列表隶属的用户的 id
                    var navUserId = localStorage.getItem(NAV_USER_ID_KEY);

                    if (navUserId == _.user.id) {
                        // 如果已经保存到 localStorage 的导航列表属于当前用户，则从 localStorage 中加载导航列表
                        var s = localStorage.getItem(NAV_LIST_KEY);

                        if (s && s.length) {
                            try {
                                apps = JSON.parse($$.util.string.fromBase64(s));
                            } catch (e) {
                                apps = null;
                            }
                        }
                    }
                }

                if (apps && apps.length) {
                    // 从本地 localStorage 中加载了导航列表
                    createNavList(apps);
                } else {
                    localStorage.removeItem(NAV_LIST_KEY);
                    localStorage.removeItem(NAV_USER_ID_KEY);
                    // 从服务器加载导航列表
                    fromServer();
                }
            };

            if (_.first) {
                var oldHistoryState = null,
                    // 保存在 history 中的导航列表状态
                    NAV_STATE = '__NAV__' + $$.util.uuid.v4();

                $(window).on('popstate', function (e) {
                    if (e.originalEvent.state === NAV_STATE) {
                        $nav.removeClass('pt-nav-opened');
                        $body.removeClass('pt-overlapped');

                        history.replaceState(oldHistoryState, '', '');
                    }
                });

                // 显示导航列表
                $('.pt-nav-toggle').on('click', function (e) {
                    e.preventDefault();

                    if ($nav.is('.pt-nav-opened')) {
                        history.back();
                    } else {
                        $nav.addClass('pt-nav-opened');
                        $body.addClass('pt-overlapped');

                        // 保存原来的 history.state
                        oldHistoryState = history.state;

                        // 设置历史状态为当前导航面板
                        history.replaceState(NAV_STATE, '', '');

                        // 添加一个新的历史记录
                        history.pushState(null, '', '');
                    }
                });

                // 点击导航背景时关闭导航列表
                $('body>nav>div').on('click', function () {
                    history.back();
                });

                // 刷新导航列表
                $('.pt-nav-refresh').on('click', function (e) {
                    e.preventDefault();
                    loadNav(true);
                });

                // 固定或取消固定
                $('.pt-nav-pin').on('click', function (e) {
                    e.preventDefault();
                    var $me = $(this);

                    if ($me.css('display') == 'none') {
                        return;
                    }

                    if ($body.is('.pt-nav-pinned')) {
                        // 取消固定
                        $body.removeClass('pt-nav-pinned');
                        $me.attr('title', '固定导航').children('i').text('call_made');
                    } else {
                        // 固定
                        $('.pt-nav-panel').one('transitionend', function () {
                            // 关闭后固定导航
                            $body.addClass('pt-nav-pinned');
                            $me.attr('title', '取消固定导航').children('i').text('call_received');
                        });
                        // 关闭导航
                        history.back();
                    }
                });

                function saveAppState(appId, collapsed) {
                    var apps = localStorage.getItem(NAV_LIST_KEY);

                    var app = null;
                    if (apps && appId) {
                        try {
                            apps = JSON.parse($$.util.string.fromBase64(apps));
                        } catch (e) {
                            apps = null;
                        }

                        if (apps) {
                            for (var i = 0; i < apps.length; i++) {
                                if (apps[i].id == appId) {
                                    app = apps[i];
                                    break;
                                }
                            }
                        }
                    }

                    if (app) {
                        app._collapsed = collapsed;
                        localStorage.setItem(NAV_LIST_KEY, $$.util.string.toBase64(JSON.stringify(apps)));
                    }
                }

                // 显示或隐藏模块列表
                $('.pt-nav-panel>section').on('click', '>.pt-nav-group>header', function () {
                    var $me = $(this);

                    // 模块列表
                    var $modList = $me.next();

                    // 正在显示或隐藏
                    if ($modList.data('transition')) {
                        return;
                    }

                    // 指示正在显示或隐藏
                    $modList.data('transition', true);

                    var appId = $me.parent().attr('data-id');

                    if ($modList.data('hidden')) {
                        // 此前已经隐藏，切换为显示
                        $modList.one('transitionend', function () {
                            $modList.data('hidden', false).removeData('transition');
                            $me.children('i').text('expand_less');
                            saveAppState(appId, false);
                            $me.parent().addClass('pt-nav-group-expanded');
                        }).css('max-height', ($modList.children().length * ITEM_MAX_HEIGHT) + 'px');
                    } else {
                        // 此前已经显示，切换为隐藏
                        $modList.one('transitionend', function () {
                            $modList.data('hidden', true).removeData('transition');
                            $me.children('i').text('expand_more');
                            saveAppState(appId, true);
                            $me.parent().removeClass('pt-nav-group-expanded');
                        }).css('max-height', '0px');
                    }
                });
            }

            // 加载导航列表
            loadNav();
        }

        // 为匿名用户加载导航列表
        function navOnAnonymous(_) {
            localStorage.removeItem(NAV_LIST_KEY);
            localStorage.removeItem(NAV_USER_ID_KEY);

            $nav.removeClass('pt-nav-opened');
            // 清空导航列表
            $('.pt-nav-panel>section').empty();
        }

        // 为登录用户加载通知中心
        function ncOnAuthorized(_) {
            var
                // 两次查询未读通知计数的时间间隔
                INTERVAL = 60000,
                // 计时器的 Id
                TIMEOUT_ID = undefined,
                // 计时器的 Id
                INTERVAL_ID = undefined,
                // 显示计数的元素
                $count = $('.pt-nc-toggle>b'),
                // 通知 DOM 列表容器
                $list = $('.pt-nc-panel>section'),
                // 紧急程度图标
                STARS = ['', 'star_border', 'star_half', '', 'star'],
                // 单个通知摘要最大高度 100 px，加上发件人、时间、主题以及 margin-bottom，最大 216px
                ITEM_MAX_HEIGHT = 216;

            // 从服务器查询当前用户接收到的新通知的数目
            function getCount(first) {
                $$.web.ajax({
                    url: '~/notification/count',
                    success: function (n) {
                        if (typeof n == 'string') {
                            n = parseInt(n, 10);
                        }

                        n = !n || Number.isNaN(n) || n < 0 ? 0 : n;

                        // 更新计数
                        $count.text(n > 999 ? '999+' : n).css('display', n > 0 ? 'block' : 'none').data('n', n);

                        if (first) {
                            clearTimeout(TIMEOUT_ID);
                            TIMEOUT_ID = undefined;

                            // 启动后续计时器，每隔 60 秒从服务器查询新通知数目
                            INTERVAL_ID = setInterval(function () {
                                getCount(false);
                            }, INTERVAL);
                        }
                    },
                    error: function (s) {
                        // 查询新通知数目发生错误时，关闭计时器
                        if (TIMEOUT_ID) {
                            clearTimeout(TIMEOUT_ID);
                            TIMEOUT_ID = undefined;
                        }

                        if (INTERVAL_ID) {
                            clearInterval(INTERVAL_ID);
                            INTERVAL_ID = undefined;
                        }

                        console.log(s);
                    }
                });
            }

            // 创建通知记录的 DOM
            function createItem(n, $gl) {
                var $n = $(
                    '<article class="pt-nc-item pt-nc-urgency-' + n.urgency + '">' +
                    '<header>' +
                    '<span></span>' +
                    '<div><b></b><time></time></div>' +
                    '<a class="mdc-ripple-surface" data-mdc-ripple-is-unbounded data-mdc-auto-init="MDCRipple">' +
                    '<i class="material-icons">cancel</i>' +
                    '</a>' +
                    '</header>' +
                    '<a class="mdc-ripple-surface" data-mdc-auto-init="MDCRipple" href="#">' +
                    (n.title ? '<b></b>' : '') +
                    '<i class="material-icons">' + STARS[n.urgency] + '</i>' +
                    '<s></s>' +
                    '</a>' +
                    '</article>');

                // 发送人头像
                if (n.senderFaceUrl) {
                    $n.find('>header>span').html('<img src="' + $$.web.url(n.senderFaceUrl) + '" alt="" />');
                } else {
                    $n.find('>header>span').html('<i class="material-icons">account_circle</i>');
                }

                // 发送人名称和时间
                $n.find('>header>div>b').text(n.senderName || '')
                    .next().text($$.util.time.name(n.receiveTime));

                var $a = $n.children('a')
                    .attr('href', $$.web.url(n.funcUrl ? n.funcUrl : '~/com/waln/notifying/inbox/list.html'));

                // 主题
                if (n.title) {
                    $a.children('b').text(n.title);
                }

                // 摘要
                $a.children('s').text(n.summary || '');

                // 将消息的附加为 DOM 的 data 属性
                $n.data('id', n.id);

                $gl.append($n);
            }

            // 按紧急程度比较两个通知记录
            function cmpByUrgency(a, b) {
                return b.urgency - a.urgency;
            }

            // 按接收时间比较两个通知记录
            function cmpByTime(a, b) {
                return $$.util.time.from(b.receiveTime).valueOf() - $$.util.time.from(a.receiveTime).valueOf();
            }

            // 创建通知记录的分组
            function createGroup(g) {
                var $g = $(
                    '<article class="pt-nc-group">' +
                    '<header class="mdc-ripple-surface" data-mdc-auto-init="MDCRipple">' +
                    '<h2></h2>' +
                    '<i class="material-icons">expand_less</i>' +
                    '</header>' +
                    '<section></section>' +
                    '</article>');

                $g.find('>header>h2').text(g.name + ' (' + g.items.length + ')');
                if (g.name == '紧急') {
                    // 按紧急程度排序
                    $$.util.array.sort(g.items, cmpByUrgency);
                } else {
                    // 按时间排序
                    $$.util.array.sort(g.items, cmpByTime);
                }

                var $gl = $g.children('section');
                g.items.each(function (item) {
                    createItem(item, $gl);
                });

                // 将分组名称保存在 data 属性中
                $g.data('name', g.name);

                $list.append($g);

                // 初始化分组的高度
                $gl.css('max-height', ($gl.children().length * ITEM_MAX_HEIGHT) + 'px');
            }

            // 创建通知列表的 DOM
            function createNcList(v, n) {
                if (!n || !v || !v.length) {
                    return;
                }

                var
                    // 分组列表
                    groups = [],
                    // 一个分组
                    group = null,
                    // 紧急分组
                    ug = {
                        name: '紧急',
                        items: []
                    },
                    // 其他分组
                    og = {
                        name: '其他',
                        items: []
                    };

                v.each(function (item, i) {
                    if (i >= n) {
                        return false;
                    }

                    var lname = (item.group || '').toLowerCase();
                    group = item.urgency > 1 || lname == '紧急' ? ug :
                        (lname == '其他' || !lname ? og :
                            groups.find(function (g) { return g.lname == lname; }));

                    if (!group) {
                        // 没有找到分组，创建新的分组
                        group = {
                            name: item.group, // 分组名称
                            lname: lname,
                            items: [] // 分组中的通知列表
                        };

                        groups.push(group);
                    }

                    group.items.push(item);
                });

                // 对分组列表进行排序
                $$.util.array.sort(groups, function (x, y) {
                    return $$.util.string.compareAsc(x.name, y.name);
                });

                // 创建紧急分组
                if (ug.items.length > 0) {
                    createGroup(ug);
                }

                // 创建分组列表
                groups.each(createGroup);

                // 创建其他分组
                if (og.items.length > 0) {
                    createGroup(og);
                }

                // 检查用户头像图片是否加载成功，如果不成功，则显示默认的图标
                $list.find('img').one('error', function () {
                    this.parentElement.innerHTML = '<i class="material-icons">account_circle</i>';
                });

                // 初始化 mdc
                $list.mdcInit();
            }

            // 从服务器查询新通知的列表
            function getList() {
                // 显示正在加载的动画
                var $div = $('<div><i class="material-icons spin">toys</i></div>').appendTo($list.empty());

                $$.web.ajax({
                    url: '~/notification/list',
                    success: function (res) {
                        if (!res) {
                            res = {
                                total: 0,
                                notifications: []
                            };
                        }

                        var n = res.total;
                        n = typeof n === 'number' ? n : parseInt(n, 10);

                        n = !n || Number.isNaN(n) || n < 0 ? 0 : n;

                        res.notifications = Array.isArray(res.notifications) ? res.notifications : [];

                        n = Math.min(n, res.notifications.length);

                        // 更新计数
                        $count.text(n > 999 ? '999+' : n).css('display', n > 0 ? 'block' : 'none').data('n', n);

                        if (n > 0) {
                            $list.empty();
                            // 创建通知的 DOM
                            createNcList(res.notifications, n);
                            $('.pt-nc-panel>header>h1').text('通知中心 (' + n + ')');
                        } else {
                            $div.text('没有通知');
                            $('.pt-nc-panel>header>h1').text('通知中心');
                        }
                    },
                    error: function (s) {
                        $div.text(s);
                    }
                });
            }

            // 在标记为已读操作完成后
            function onRead($n) {
                var
                    // 隶属列表
                    $l = $n.parent(),
                    // 隶属分组
                    $g = $l.parent();

                // 移除通知的 DOM
                $n.remove();

                var c = $l.children().length;
                // 如果列表已经为空，则移除分组
                if (c == 0) {
                    $g.remove();
                } else {
                    // 减少分组标题中的计数
                    $g.find('>header>h2').text($g.data('name') + ' (' + c + ')');
                }

                // 更新提示数字
                c = Math.max($count.data('n') - 1, 0);
                $count.text(c > 999 ? '999+' : c).css('display', c > 0 ? 'block' : 'none').data('n', c);
                // 标题
                $('.pt-nc-panel>header>h1').text(c > 0 ? ('通知中心 (' + c + ')') : '通知中心');
            }

            // 将指定的消息标记为已读
            function read($n) {
                $$.web.ajax({
                    url: '~/notification/read',
                    data: function () {
                        return {
                            id: $n.data('id')
                        };
                    },
                    success: function () {
                        onRead($n);
                    },
                    error: function (s) {
                        console.log(s);
                        // 在标记发生错误时，也移除 DOM，以便给用户视觉提示
                        onRead($n);
                    }
                });
            }

            if (_.first) {
                var oldHistoryState = null, NC_STATE = '__NC__' + $$.util.uuid.v4();

                $(window).on('popstate', function (e) {
                    if (e.originalEvent.state === NC_STATE) {
                        $nc.removeClass('pt-nc-opened');
                        $body.removeClass('pt-overlapped');

                        history.replaceState(oldHistoryState, '', '');

                        // 关闭通知中心后重启计时器
                        if (!INTERVAL_ID) {
                            INTERVAL_ID = setInterval(function () {
                                getCount(false);
                            }, INTERVAL);
                        }
                    }
                });

                // 打开通知中心
                $('.pt-nc-toggle').on('click', function (e) {
                    e.preventDefault();

                    if ($nc.is('.pt-nc-opened')) {
                        history.back();
                    } else {
                        // 打开通知中心时，停止计时器
                        if (TIMEOUT_ID) {
                            clearTimeout(TIMEOUT_ID);
                            TIMEOUT_ID = undefined;
                        }

                        if (INTERVAL_ID) {
                            clearInterval(INTERVAL_ID);
                            INTERVAL_ID = undefined;
                        }

                        // 只有当前列表中的数目与提示的数目不一致，才会刷新通知消息列表
                        if ($list.find('.pt-nc-item').length != $count.data('n')) {
                            // 从服务器查询列表
                            getList();
                        }

                        $nc.addClass('pt-nc-opened');
                        $body.addClass('pt-overlapped');

                        // 保存原来的 history.state
                        oldHistoryState = history.state;

                        // 设置历史状态为当前导航面板
                        history.replaceState(NC_STATE, '', '');

                        // 添加一个新的历史记录
                        history.pushState(null, '', '');
                    }
                });

                // 关闭通知中心
                $('.pt-nc-close,body>aside>div').on('click', function (e) {
                    e.preventDefault();
                    history.back();
                });

                // 显示或隐藏分组内的通知列表
                $('.pt-nc-panel>section').on('click', '>.pt-nc-group>header', function () {
                    var $me = $(this);

                    // 分组内的通知列表
                    var $list = $me.next();

                    // 正在显示或隐藏
                    if ($list.data('transition')) {
                        return;
                    }

                    // 指示正在显示或隐藏
                    $list.data('transition', true);

                    if ($list.data('hidden')) {
                        // 此前已经隐藏，切换为显示
                        $list.one('transitionend', function () {
                            $list.data('hidden', false).removeData('transition');
                            $me.children('i').text('expand_less');
                        }).css('max-height', ($list.children().length * ITEM_MAX_HEIGHT) + 'px');
                    } else {
                        // 此前已经显示，切换为隐藏
                        $list.one('transitionend', function () {
                            $list.data('hidden', true).removeData('transition');
                            $me.children('i').text('expand_more');
                        }).css('max-height', '0px');
                    }
                });

                // 将消息标记为已读
                $list.on('click', '.pt-nc-item>header>a', function (e) {
                    e.preventDefault();
                    // 将当前 DOM 对应的消息标记为已读
                    read($(this).closest('.pt-nc-item'));
                });

                // 窗口卸载时关闭计时器
                $(window).on('unload', function () {
                    // 停止计时器
                    if (TIMEOUT_ID) {
                        clearTimeout(TIMEOUT_ID);
                        TIMEOUT_ID = undefined;
                    }

                    if (INTERVAL_ID) {
                        clearInterval(INTERVAL_ID);
                        INTERVAL_ID = undefined;
                    }
                });
            }

            // 第一次查询通知数目是 2 秒后，以后则每隔 60 秒查询一次
            TIMEOUT_ID = setTimeout(function () {
                getCount(true);
            }, 2000);
        }

        // 为匿名用户加载通知中心
        function ncOnAnonymous(_) {
            if ($nc.length > 0) {
                $nc.removeClass('pt-nc-opened');
                // 清空通知列表
                $('.pt-nc-panel>section').empty();
            }
        }

        $$({
            // 当用户信息加载成功并且用户不是匿名用户时，或者用户登录成功后执行的函数。
            authorized: function (_) {
                $('.pt-nav-toggle,.pt-nc-toggle,.pt-page-menu-logout').css('display', 'flex');
                $('.pt-page-menu-logout-divider').css('display', 'list-item');

                // 用户信息
                (function () {
                    // 用户头像
                    if (_.user.faceUrl) {
                        $('.pt-nav-userbar>a')
                            .html('<img src="' + $$.web.url(_.user.faceUrl) + '" alt="" /><b></b>')
                            .children('img').one('error', function () {
                                $('.pt-nav-userbar>a').html('<i class="material-icons">account_circle</i><b></b>');
                                // 用户名称
                                $('.pt-nav-userbar>a>b').text(_.user.name);
                            });
                    } else {
                        $('.pt-nav-userbar>a').html('<i class="material-icons">account_circle</i><b></b>');
                    }

                    // 用户名称
                    $('.pt-nav-userbar>a>b').text(_.user.name);
                })();

                // 导航列表
                if ($nav.length > 0 && $('.pt-nav-toggle').length > 0) {
                    navOnAuthorized(_);
                }

                // 通知中心
                if ($nc.length > 0 && $('.pt-nc-toggle').length > 0) {
                    ncOnAuthorized(_);
                }
            },

            // 当用户信息加载成功并且用户是匿名用户时，或者用户登录成功后执行的函数。
            anonymous: function (_) {
                $('.pt-nav-toggle,.pt-nc-toggle,.pt-page-menu-logout,.pt-page-menu-logout-divider').css('display', 'none');

                // 用户信息
                (function () {
                    // 用户头像
                    $('.pt-nav-userbar>a').html('<i class="material-icons">account_circle</i><b></b>');
                    // 用户名称
                    $('.pt-nav-userbar>a>b').text('未登录');
                })();

                // 导航列表
                if ($nav.length > 0 && $('.pt-nav-toggle').length > 0) {
                    navOnAnonymous(_);
                }

                // 通知中心
                if ($nc.length > 0 && $('.pt-nc-toggle').length > 0) {
                    ncOnAnonymous(_);
                }
            }
        });
    })();

    // 初始化
    $(function () {
        // 与用户无关的初始化操作
        (function () {
            // 在加载用户信息前，隐藏只有非匿名用户可见的内容
            $('.pt-nav-toggle,.pt-nc-toggle,.pt-page-menu-logout,.pt-page-menu-logout-divider').css('display', 'none');

            // text
            (function () {
                const ALL = 'textarea,select,input[type="text"],input[type="password"],input[type="color"],input[type="date"],' +
                    'input[type="datetime-local"],input[type="email"],input[type="month"],input[type="number"],input[type="search"],' +
                    'input[type="tel"],input[type="time"],input[type="url"],input[type="week"]',
                    FS_ALL = '.pt-field>label>textarea,.pt-field>label>select,.pt-field>label>input[type="text"],.pt-field>label>input[type="password"],' +
                        '.pt-field>label>input[type="color"],.pt-field>label>input[type="date"],.pt-field>label>input[type="datetime-local"],' +
                        '.pt-field>label>input[type="email"],.pt-field>label>input[type="month"],.pt-field>label>input[type="number"],' +
                        '.pt-field>label>input[type="search"],.pt-field>label>input[type="tel"],.pt-field>label>input[type="time"],' +
                        '.pt-field>label>input[type="url"],.pt-field>label>input[type="week"]';
                $body
                    .on('change', ALL, function () {
                        var $t = $(this);
                        $t.val($t.val().trim($t.attr('data-trim')));
                    })
                    .on('focusin', FS_ALL, function () {
                        $(this).closest('.pt-field').addClass('focused');
                    })
                    .on('focusout', FS_ALL, function () {
                        $(this).closest('.pt-field').removeClass('focused');
                    });
            })();

            // 页面菜单
            (function () {
                var $menu = $('body>header .mdc-simple-menu'),
                    menu = $menu.length > 0 ? new mdc.menu.MDCSimpleMenu($menu[0]) : null;
                if (menu) {
                    $menu.prev().on('click', function (e) {
                        e.preventDefault();
                        menu.open = !menu.open;
                    });
                    
                    $menu.on('MDCSimpleMenu:selected', function (e) {
                        switch (e.detail.index) {
                            case 0:
                                // 刷新页面
                                window.location.reload(true);
                                break;
                            case 2:
                                // 退出
                                $$.user.logout();
                                break;
                        }
                    });
                }
            })();

            // pt-row
            (function () {
                function triggerRowEvent($row, type, model, selected) {
                    var event = $.Event(type);

                    Object.defineProperty(event, 'model', {
                        get: function () {
                            return model;
                        }
                    });

                    Object.defineProperty(event, 'selected', {
                        get: function () {
                            return selected;
                        }
                    });

                    if (type == 'beforeselect') {
                        // 是否取消变化
                        event.cancel = false;
                        // 是否应该同时选中或取消选中子级
                        event.affectChildren = false;
                        // 是否应该同时选中或取消选中父级
                        event.affectParent = false;
                    }

                    $row.trigger(event);

                    return event;
                }

                function toggleSelect($row, selected, dir) {
                    if ($row.length == 0 || $row.is('.pt-row-selected') === selected) {
                        return false;
                    }

                    var
                        $toggle = $row.find('>header .pt-row-toggle'),
                        model = $row.data('model'),
                        beforeEvent = triggerRowEvent($row, 'beforeselect', model, selected);

                    if (beforeEvent.cancel) {
                        return false;
                    }

                    if ((dir === 'down' || dir === 'updown') && beforeEvent.affectChildren) {
                        // 同时设置子级行
                        $row.find('>section>.pt-row').each(function () {
                            toggleSelect($(this), selected, 'down');
                        });
                    }

                    if ((dir === 'up' || dir === 'updown') && beforeEvent.affectParent) {
                        // 检查父级
                        var $parent = $row.parent().closest('.pt-row');
                        if ($parent.length > 0) {
                            // 确定父级的新状态
                            var parentSelected = true;
                            if (selected) {
                                // 选中时，需要检查同级的所有条目是否都被选中，如果都被选中，则将 $pall 设置为选中
                                $parent.find('>section>.pt-row').each(function () {
                                    if (this === $row[0]) {
                                        return;
                                    }

                                    if (!$(this).is('.pt-row-selected')) {
                                        parentSelected = false;
                                        return false;
                                    }
                                });
                            } else {
                                parentSelected = false;
                            }

                            toggleSelect($parent, parentSelected, 'up');
                        }
                    }

                    $row.toggleClass('pt-row-selected', selected);
                    $toggle.text(selected ?
                        ($toggle.attr('data-on-icon') || 'check_box') :
                        ($toggle.attr('data-off-icon') || 'check_box_outline_blank'));

                    // 触发 afterselect 事件
                    triggerRowEvent($row, 'afterselect', model, selected);

                    return true;
                }

                // 将指定的行设置为已经展开或折叠
                function toggleExpand($row, expand, clearChildren, $header) {
                    if (expand === !!$row.is('.pt-row-expanded')) {
                        // 前后没有变化
                        return;
                    }

                    var $children = $row.children('section'),
                        $mi = null;

                    if ($children.length == 0) {
                        // 没有子级列表
                        return;
                    }

                    if (clearChildren) {
                        // 清空子级
                        $children.empty();
                        $row.data('__children_loaded__', false);
                    }

                    $header = $header || $row.children('header');

                    // 查找展开或折叠的图标指示
                    $header.find('>.pt-row-more>.material-icons').each(function () {
                        var $i = $(this),
                            t = $i.text().toLowerCase();
                        if (t == 'expand_more' || t == 'expand_less') {
                            $mi = $i;
                            return false;
                        }
                    });

                    // 展开
                    function doExpand() {
                        $mi.text('expand_less');
                        $children.css('display', '');
                        $row.addClass('pt-row-expanded')
                            .trigger('expanded');
                    }

                    // 折叠
                    function doCollapse() {
                        $mi.text('expand_more');
                        $children.css('display', 'none');
                        $row.removeClass('pt-row-expanded')
                            .trigger('collapsed');
                    }

                    if (!expand) {
                        // 折叠
                        doCollapse();
                    } else {
                        // 展开，展开之前首先尝试加载子级
                        if (!$row.data('__children_loaded__')) {
                            // 触发 children 事件
                            var evt = $.Event('children');

                            Object.defineProperty(evt, 'model', {
                                get: function () {
                                    return $row.data('model');
                                }
                            });

                            Object.defineProperty(evt, 'childrenLoaded', {
                                get: function () {
                                    return !!$row.data('__children_loaded__');
                                },
                                set: function (val) {
                                    var loaded = !!val;
                                    $row.data('__children_loaded__', loaded);

                                    if (loaded) {
                                        doExpand();
                                    }
                                }
                            });

                            $row.trigger(evt);
                        } else {
                            doExpand();
                        }
                    }
                }

                // 扩展 jQuery，添加一个选中行的函数
                $.fn.rowSelected = function () {
                    var me = this;

                    if (arguments.length < 1) {
                        // 获取行是否被选中的指示
                        return this.is('.pt-row-selected');
                    } else {
                        var selected = !!$$.val(arguments[0], null, me);
                        // 设置行是否被选中的指示
                        me.each(function () {
                            var $row = $(this);
                            toggleSelect($row, selected, 'updown');
                        });

                        return me;
                    }
                };

                // 展开或折叠一个行
                $.fn.rowExpanded = function () {
                    var me = this;

                    if (arguments.length < 1) {
                        // 获取行是否被选中的指示
                        return this.is('.pt-row-expanded');
                    } else {
                        var expand = !!$$.val(arguments[0], null, me),
                            clearChildren = arguments.length < 2 ? false : !!$$.val(arguments[1], null, me);
                        // 设置行是否被选中的指示
                        me.each(function () {
                            var $row = $(this);
                            toggleExpand($row, expand, clearChildren);
                        });

                        return me;
                    }
                };

                $('body,.mdc-dialog__body')
                    .on('click', '.pt-row>header .pt-row-toggle,.pt-row>header a,.pt-row>header button', function (e) {
                        // 如果行是可以展开和折叠的，默认的取消按钮和链接的冒泡，除非指定了 [data-bubble]
                        var $me = $(this);
                        if ($me.is('[data-bubble]')) {
                            return;
                        }

                        // 检查行是否可以展开和折叠
                        var $mis = $me.closest('header').find('>.pt-row-more>.material-icons'),
                            canExpand = false;

                        $mis.each(function () {
                            var text = $(this).text().toLowerCase();
                            if (text == 'expand_more' || text == 'expand_less') {
                                canExpand = true;
                                return false;
                            }
                        });

                        if (canExpand) {
                            // 可以展开或折叠，此时取消冒泡
                            e.stopPropagation();
                        }
                    })
                    .on('click', '.pt-row-toggle', function () {
                        var $toggle = $(this),
                            $row = $toggle.closest('.pt-row');

                        if ($toggle.is('.disabled')) {
                            // 已经禁用
                            return;
                        }

                        toggleSelect($row, !$row.is('.pt-row-selected'), 'updown');
                    })
                    .on('click', '.pt-row>header', function () {
                        var $h = $(this),
                            $row = $h.parent();
                        toggleExpand($row, !$row.is('.pt-row-expanded'), false, $h);
                    });
            })();

            // dialog
            (function () {
                $body
                    // 打开对话框
                    .on('click', '[data-target-dialog]', function (e) {
                        e.preventDefault();

                        var
                            // 触发打开对话框的按钮
                            $button = $(this),
                            // 目标对话框
                            $dlg = $($button.attr('data-target-dialog'));

                        if (!$dlg.length) {
                            // 没有对话框元素
                            return;
                        }

                        // 检查是否已经为对话框初始化 mdc
                        if (!$dlg.data('__mdc_init__')) {
                            $dlg.mdcInit();
                            $dlg.data('__mdc_init__', true);
                        }

                        // 检查当前按钮是否位于某一个 pt-model 内部
                        var
                            // 按钮隶属的 pt-model
                            $model = $button.closest('.pt-model'),
                            // 当前 model 对象
                            model = null;
                        if ($model.length > 0) {
                            // 最近的 model 对象
                            model = $model.data('model');
                        }

                        if (!model) {
                            // 如果没有找到 pt-model，则从对话框自身查找 model 对象
                            model = $dlg.data('model');
                        }

                        // 为对话框触发 opening 事件
                        var openingEvt = $.Event('opening');

                        // 为事件添加一个 model 属性
                        Object.defineProperty(openingEvt, 'model', {
                            get: function () {
                                return model;
                            },
                            set: function (value) {
                                model = value;
                            }
                        });

                        // 为事件添加一个 $button 属性
                        Object.defineProperty(openingEvt, '$button', {
                            get: function () {
                                return $button;
                            }
                        });

                        // 为事件添加一个 cancel 属性
                        openingEvt.cancel = false;

                        // 触发 opening
                        $dlg.trigger(openingEvt);

                        if (openingEvt.cancel) {
                            // 取消打开对话框
                            return;
                        }

                        var
                            // 查找此前已经创建的 MDCDialog
                            mdcDialog = $dlg.data('MDCDialog'),
                            // 对话框按钮区域
                            $dlgFooter = $dlg.find('.mdc-dialog__footer'),
                            // 确定按钮
                            $btnOK = $dlgFooter.find('[data-ok]'),
                            // 取消按钮
                            $btnCancel = $dlgFooter.find('[data-cancel]'),
                            // 原历史状态
                            oldHistoryState = null,
                            // 保存在 history 中的对话框状态
                            DIALOG_STATE = '__DIALOG__' + $$.util.uuid.v4(),
                            // 是否点击了确定按钮
                            isAccept = false;

                        if (!mdcDialog) {
                            // 此前尚未创建过 MDCDialog
                            mdcDialog = new mdc.dialog.MDCDialog($dlg[0]);
                            $dlg.data('MDCDialog', mdcDialog);
                        }

                        // 为对话框添加 ok 事件
                        if ($btnOK.length) {
                            // 必须是一个一次性的事件
                            $btnOK.one('click', function () {
                                // 触发一个自定义的 ok 事件
                                var okEvt = $.Event('ok');

                                // 为事件添加一个 model 属性
                                Object.defineProperty(okEvt, 'model', {
                                    get: function () {
                                        return model;
                                    }
                                });

                                // 为事件添加一个 $button 属性
                                Object.defineProperty(okEvt, '$button', {
                                    get: function () {
                                        return $button;
                                    }
                                });

                                var _done = false;
                                // 添加一个自定义的 done 属性，如果 done 设置为 true，则关闭对话框
                                Object.defineProperty(okEvt, 'done', {
                                    get: function () {
                                        return _done;
                                    },
                                    set: function (done) {
                                        _done = !!done;
                                        if (_done) {
                                            // 当 done 设置为 true 时，关闭对话框
                                            isAccept = true;
                                            history.back();
                                        }
                                    }
                                });

                                $dlg.trigger(okEvt);
                            });
                        }

                        // 为对话框添加 cancel 事件
                        if ($btnCancel.length) {
                            $btnCancel.one('click', function () {
                                isAccept = false;
                                history.back();
                            });
                        }

                        // 当回退时关闭对话框
                        $(window).one('popstate', function (e) {
                            if (e.originalEvent.state === DIALOG_STATE) {
                                if (isAccept) {
                                    // 点击了确定按钮
                                    mdcDialog.getDefaultFoundation().accept(true);
                                } else {
                                    // 点击了取消按钮
                                    mdcDialog.getDefaultFoundation().cancel(true);
                                }
                                history.replaceState(oldHistoryState, '', '');
                            }
                        });

                        // 保存原来的 history.state
                        oldHistoryState = history.state;

                        // 设置历史状态为当前导航面板
                        history.replaceState(DIALOG_STATE, '', '');

                        // 添加一个新的历史记录
                        history.pushState(null, '', '');

                        // 为对话框添加 accept 和 cancel 事件
                        function onAccept() {
                            mdcDialog.unlisten('MDCDialog:accept', onAccept);
                            mdcDialog.unlisten('MDCDialog:cancel', onCancel);

                            // 触发一个自定义的 accept 事件
                            var acceptEvt = $.Event('accept');

                            // 为事件添加一个 model 属性
                            Object.defineProperty(acceptEvt, 'model', {
                                get: function () {
                                    return model;
                                }
                            });

                            // 为事件添加一个 $button 属性
                            Object.defineProperty(acceptEvt, '$button', {
                                get: function () {
                                    return $button;
                                }
                            });

                            $dlg.trigger(acceptEvt);
                        }

                        function onCancel() {
                            mdcDialog.unlisten('MDCDialog:accept', onAccept);
                            mdcDialog.unlisten('MDCDialog:cancel', onCancel);

                            // 触发一个自定义的 cancel 事件
                            var cancelEvt = $.Event('cancel');

                            // 为事件添加一个 model 属性
                            Object.defineProperty(cancelEvt, 'model', {
                                get: function () {
                                    return model;
                                }
                            });

                            // 为事件添加一个 $button 属性
                            Object.defineProperty(cancelEvt, '$button', {
                                get: function () {
                                    return $button;
                                }
                            });

                            $dlg.trigger(cancelEvt);
                        }

                        // 为对话框注册一次性的 accept 和 cancel 事件
                        mdcDialog.listen('MDCDialog:accept', onAccept);
                        mdcDialog.listen('MDCDialog:cancel', onCancel);

                        // 显示对话框
                        mdcDialog.lastFocusedTarget = e.target;
                        mdcDialog.show();

                        // 为对话框触发 opened 事件
                        var openedEvt = $.Event('opened');

                        // 为事件添加一个 model 属性
                        Object.defineProperty(openedEvt, 'model', {
                            get: function () {
                                return model;
                            }
                        });

                        // 为事件添加一个 $button 属性
                        Object.defineProperty(openedEvt, '$button', {
                            get: function () {
                                return $button;
                            }
                        });

                        $dlg.trigger(openedEvt);
                    });
            })();

            // menu
            (function () {
                var globalOpenedMenu = null;
                $body
                    // 打开菜单
                    .on('click', '[data-target-menu]', function (e) {
                        e.preventDefault();

                        var
                            // 触发打开菜单的按钮
                            $button = $(this),
                            // 目标菜单
                            $menu = $($button.attr('data-target-menu'));

                        if (!$menu.length) {
                            // 没有菜单元素
                            return;
                        }

                        // 检查是否已经为菜单初始化 mdc
                        if (!$menu.data('__mdc_init__')) {
                            $menu.mdcInit();
                            $menu.data('__mdc_init__', true);
                        }

                        var
                            // 查找此前已经创建的 MDCSimpleMenu
                            mdcMenu = $menu.data('MDCSimpleMenu');

                        if (!mdcMenu) {
                            // 此前尚未创建过 MDCSimpleMenu
                            mdcMenu = new mdc.menu.MDCSimpleMenu($menu[0]);
                            $menu.data('MDCSimpleMenu', mdcMenu);

                            // 如果浏览器窗口尺寸发生改变，菜单的位置可能超出了当前视图，需要将菜单关闭
                            $(window).on('resize', function () {
                                if (mdcMenu.open) {
                                    mdcMenu.open = false;
                                    mdcMenu.foundation_.adapter_.notifyCancel();
                                    globalOpenedMenu = null;
                                }
                            });

                            $menu.find('.mdc-list-item').on('click', function (evt) {
                                var item = this,
                                    $item = $(item),
                                    dialog = $item.attr('data-target-dialog'),
                                    menuModel = $menu.data('model'),
                                    $menuButton = $menu.data('$button'),
                                    targetIndex = mdcMenu.foundation_.adapter_.getIndexForEventTarget(item);

                                // 检查是否存在与菜单项关联的对话框，如果存在，则设置对话框的 model
                                // 随后在对话框的 opening、ok、cancel、opened 等事件中可以使用该 model 对象
                                if (dialog) {
                                    $(dialog).data('model', menuModel);
                                }

                                if (targetIndex >= 0) {
                                    if ($item.is('.disabled')) {
                                        // 被禁用的项目不执行任何操作
                                        evt.stopPropagation();
                                        evt.preventDefault();
                                        return;
                                    }

                                    // 为菜单触发自定义的 selected 事件
                                    var selectedEvt = $.Event('selected');

                                    // 为事件添加一个 model 属性
                                    Object.defineProperty(selectedEvt, 'model', {
                                        get: function () {
                                            return menuModel;
                                        }
                                    });

                                    // 为事件添加一个 $button 属性
                                    Object.defineProperty(selectedEvt, '$button', {
                                        get: function () {
                                            return $menuButton;
                                        }
                                    });

                                    // 为事件添加一个 selectedIndex 属性
                                    Object.defineProperty(selectedEvt, 'selectedIndex', {
                                        get: function () {
                                            return targetIndex;
                                        }
                                    });

                                    // 为事件添加一个 selectedIndex 属性
                                    Object.defineProperty(selectedEvt, 'selectedItem', {
                                        get: function () {
                                            return item;
                                        }
                                    });

                                    $menu.trigger(selectedEvt);
                                }
                            });
                        }

                        // 如果菜单此前已经打开，则首先关闭
                        if (mdcMenu.open) {
                            mdcMenu.open = false;
                            mdcMenu.foundation_.adapter_.notifyCancel();
                            globalOpenedMenu = null;
                        }

                        if (globalOpenedMenu) {
                            if (globalOpenedMenu.open) {
                                // 如果存在其他已经打开的菜单，则关闭
                                globalOpenedMenu.open = false;
                                globalOpenedMenu.foundation_.adapter_.notifyCancel();
                            }
                            globalOpenedMenu = null;
                        }

                        // 检查当前按钮是否位于某一个 pt-model 内部
                        var
                            // 按钮隶属的 pt-model
                            $model = $button.closest('.pt-model'),
                            // 当前 model 对象
                            model = null;
                        if ($model.length > 0) {
                            // 最近的 model 对象
                            model = $model.data('model');
                        }

                        // 将 model 和 $button 关联到 $menu
                        $menu.data('model', model).data('$button', $button);

                        // 为菜单触发 opening 事件
                        var openingEvt = $.Event('opening');

                        // 为事件添加一个 model 属性
                        Object.defineProperty(openingEvt, 'model', {
                            get: function () {
                                return model;
                            }
                        });

                        // 为事件添加一个 $button 属性
                        Object.defineProperty(openingEvt, '$button', {
                            get: function () {
                                return $button;
                            }
                        });

                        // 为事件添加一个 items 属性
                        Object.defineProperty(openingEvt, 'items', {
                            get: function () {
                                return mdcMenu.items;
                            }
                        });

                        // 为事件添加一个 cancel 属性
                        openingEvt.cancel = false;

                        // 触发 opening
                        $menu.trigger(openingEvt);

                        if (openingEvt.cancel) {
                            // 取消打开菜单
                            return;
                        }

                        // 按钮的位置
                        var offset = $button.offset();
                        // 菜单的位置
                        $menu.css({
                            'top': (offset.top + $button.height()) + 'px',
                            'left': (offset.left - $menu.width() - 16) + 'px'
                        });

                        // 打开菜单
                        mdcMenu.open = true;
                        // 记录打开的菜单
                        globalOpenedMenu = mdcMenu;

                        // 为菜单触发 opened 事件
                        var openedEvt = $.Event('opened');

                        // 为事件添加一个 model 属性
                        Object.defineProperty(openedEvt, 'model', {
                            get: function () {
                                return model;
                            }
                        });

                        // 为事件添加一个 $button 属性
                        Object.defineProperty(openedEvt, '$button', {
                            get: function () {
                                return $button;
                            }
                        });

                        // 为事件添加一个 items 属性
                        Object.defineProperty(openedEvt, 'items', {
                            get: function () {
                                return mdcMenu.items;
                            }
                        });

                        $menu.trigger(openedEvt);
                    });
            })();

            // switch
            (function () {
                $body.on('change', '.pt-field .mdc-switch>.mdc-switch__native-control', function () {
                    $('#' + this.id + '-text')
                        .text($(this.parentElement)
                            .attr('data-' + this.checked + '-text') || (this.checked ? '是' : '否'));
                });
            })();
        })();

        $$.user.load();
    });
})(jQuery, jQuery(document));
